This document is the working visual and accessibility contract for the SafeRide mobile app. It replaces the previous sector-agnostic style notes with a SafeRide-specific system for safety reporting, privacy-preserving evidence workflows, offline legal aid, and Kenyan public-transport contexts.
Implementation source of truth:
- Runtime tokens:
src/theme/tokens.ts - App theme bridge:
src/theme/SimpleThemeProvider.tsx - Legacy provider shim:
src/theme/ThemeProvider.tsx - Usage and visual QA guide:
docs/migration/design-system-usage-visual-qa.md - Manual fixture surface:
src/screens/__fixtures__/DesignSystemFixtures.tsx
Scope for this foundation: tokens, theme behavior, accessibility baselines, and migration guidance. Screen restyling and component rewrites belong in follow-up UI implementation issues.
SafeRide should feel calm, discreet, trustworthy, and warm. The interface must support high-stress reporting moments without using alarm-heavy colors as the default visual language. The light theme uses a soft blush canvas, white surfaces, plum trust actions, softened teal safety states, clay consent cues, and restrained blue evidence cues so the app feels comforting without becoming a one-note pink or beige product. Safety states, consent prompts, evidence handling, offline status, and privacy controls have distinct semantic colors so screens can communicate state without relying on one blue brand color.
The light theme is the primary production mode. Dark mode and high-contrast mode are scaffolded through the same semantic tokens so future screens can adopt them without replacing local styles twice.
| Mode | Status | Intent |
|---|---|---|
light |
Primary | Default SafeRide UI. Warm blush canvas, white surfaces, plum trust primary, separate safety/evidence/privacy states. |
dark |
Scaffolded | Low-light mode with the same semantic roles and Paper theme mapping. Validate screen-by-screen before defaulting to system dark mode in production. |
highContrast |
Scaffolded | Explicit high-contrast theme for manual accessibility settings. Uses black borders, strong text contrast, and a black focus ring. |
system |
Available preference | Resolves to light or dark from Appearance when explicitly selected. The provider defaults to light until dark-mode QA and a user setting are wired. High contrast remains explicit until an accessibility preference is wired. |
Use mode from useTheme() when a component needs to know the resolved design mode. Use colorScheme only for legacy light/dark branching.
All component colors should come from useTheme().colors or from themeColors[mode]. New code should not introduce raw hex values in screen styles unless the value is a temporary migration note with a named token target.
| Token | Light value | Purpose |
|---|---|---|
canvas / background |
#FFF8F3 |
App background and full-screen surfaces. |
surface / card |
#FFFFFF |
Cards, form fields, dialogs, sheets. |
surfaceAlt / muted |
#F8EFE8 |
Grouped sections, subtle fills, inactive surfaces. |
foreground / textPrimary |
#211513 |
Main text and icons. |
textSecondary |
#604A43 |
Helper copy, secondary labels, timestamps. |
textTertiary |
#80665D |
Placeholders, disabled copy, low-priority metadata. |
border / divider |
#E6D3C8 / #F0E1D7 |
Inputs, list dividers, card outlines. |
primary |
#713B5D |
Primary action, active navigation, trusted affordances. Primary buttons are solid, not gradient-led. |
primaryMuted |
#F4E5EE |
Selected chips, primary callout background. |
accent |
#F3E7F0 |
Informational panels that should not read as urgent. |
focusRing |
#713B5D |
Standard visible focus indicator in light mode. |
| State | Token set | Light value | Use when |
|---|---|---|---|
| Critical | critical, criticalForeground, criticalMuted |
#8C2430 |
Immediate risk, hard blockers, or safety-critical failure. Prefer destructive for actions and critical for state. |
| Safety | safety, safetyForeground, safetyMuted |
#0D6F63 |
Trusted safety actions, safe route/support confirmations, resolved safety checks. |
| Warning | warning, warningForeground, warningMuted |
#9A5A18 |
Time-sensitive cautions, incomplete setup, non-blocking risk. |
| Offline | offline, offlineForeground, offlineMuted |
#6A6260 |
No network, local assistant fallback, offline mode. |
| Queued | queued, queuedForeground, queuedMuted |
#6A6260 |
Per-object sync/upload queue state, pending replay, deferred remote actions. |
| Consent | consent, consentForeground, consentMuted |
#8B4F22 |
Explicit consent, legal acknowledgement, sharing permission. |
| Evidence | evidence, evidenceForeground, evidenceMuted |
#2F648C |
Evidence capture, attached media, chain-of-custody status. |
| Support | support, supportForeground, supportMuted |
#3D7652 |
Provider directory, support resources, referral availability. |
| Case | case, caseForeground, caseMuted |
#47606A |
Case timeline, current case status, submitted/local/blocked case states. |
| Success | success, successForeground, successMuted |
#2E7D4F |
Completed saves, sent reports, confirmed settings. |
| Destructive | destructive, destructiveForeground, dangerMuted |
#A9313A |
Delete, revoke, reset, unsafe failure. |
| Privacy | privacy, privacyForeground, privacyMuted |
#714268 |
Private mode, redaction, data visibility, sensitive fields. |
| Info | info, infoForeground, infoMuted |
#2F648C |
Neutral legal guidance, explainers, system updates. |
| Unavailable | unavailable, unavailableForeground, unavailableMuted |
#6A6260 |
Disabled, future, blocked, or not-implemented capabilities. |
Muted semantic colors are backgrounds. Solid semantic colors are text, icon, border, or filled-control colors. Filled controls must use the matching foreground token.
Tokens are defined in typography and use zero letter spacing by default. Current families stay compatible with the existing app, but all text must continue to respect React Native font scaling.
| Token | Size / line height | Use |
|---|---|---|
titleXL |
28 / 34, 700 | Screen titles and major empty states. |
titleL |
22 / 28, 600 | Section headers, large card titles. |
titleM |
20 / 26, 600 | Card headers and dialog headings. |
titleS |
18 / 24, 600 | Compact group labels. |
bodyL |
18 / 26, 400 | Long-form guidance where readability matters. |
bodyM |
17 / 24, 400 | Default body copy and row labels. |
bodyS |
15 / 22, 400 | Helper copy and dense metadata. |
caption |
13 / 18, 400 | Badges, timestamps, disclaimers. |
label |
15 / 20, 600 | Field labels and chip labels. |
button |
17 / 22, 600 | Buttons and primary actions. |
Do not clamp text to a single line when truncation can hide safety, consent, or legal meaning. Rows and chips should grow vertically under larger dynamic type settings.
Use spacing for all fixed gaps. The base rhythm is 4 / 8 / 12 / 16 / 20 / 24 / 32 / 40 with spacing.gutter = 16 for screen gutters.
Layout constants:
- Minimum touch target:
44pt. - Preferred touch target:
48pt. - Action height:
52pt. - List row and form field height:
56pt. - Max readable copy width:
340pt. - Docked CTA bottom inset:
16pt plus safe-area clearance.
Radii:
- Cards and framed sections:
8pt. - Inputs:
8pt. - Buttons:
10pt. - Chips/state pills:
16pt. - Sheets:
16pt. - Badges:
8pt.
Use radii.round only for circular controls or full pills. Cards, forms, rows, and framed sections should stay restrained; do not make large page surfaces look like pill bubbles.
Elevation is intentionally quiet. Use elevation.card for grouped content, elevation.floating for docked or floating actions, and elevation.sheet for modal sheets. Avoid stacking multiple elevated surfaces inside each other.
Feedback tokens:
- Pressed opacity:
0.86. - Disabled opacity:
0.42. - Selection overlay:
rgba(113,59,93,0.12). - Danger overlay:
rgba(169,49,58,0.10).
Motion durations:
- Quick state changes:
120ms. - Standard transitions:
180ms. - Deliberate transitions:
240ms. - Sheet transitions:
320ms.
Layer tokens use base, raised, header, footer, sheet, dialog, toast, and quickExit. Use them instead of ad hoc z-index values.
Motion should never be required to understand whether evidence, consent, or emergency state changed. Pair animation with text, icon, or semantic color state.
SimpleThemeProvider remains the app-level provider to avoid broad import churn. It now supplies:
const { colors, tokens, semanticStates, mode, colorScheme, setTheme, paperTheme } = useTheme();The provider also wraps children with React Native Paper's PaperProvider, using the same SafeRide color roles mapped onto MD3 theme fields. Future Paper components should not create separate Paper themes.
Use semantic state metadata when building reusable status components:
const state = semanticStates.evidence;
const backgroundColor = colors[state.muted];
const color = colors[state.color];For repeated object states, use the exported primitives before creating screen-local styles: StatePill, StatusBanner, LoadingState, UnavailableState, FormSection, EvidenceRow, ProviderRow, CaseTimelineItem, and ConsentSummary.
Contrast:
- Body text and meaningful icons: at least 4.5:1 against the background.
- Large text: at least 3:1.
- Focus rings, selected borders, and non-text indicators: at least 3:1.
- Filled semantic states must use their paired foreground tokens.
Touch and layout:
- Interactive controls must be at least 44 x 44 pt.
- Primary actions should use the 52 pt action height.
- Adjacent destructive and primary actions need clear spacing or confirmation.
- Controls must remain reachable with safe-area and keyboard insets applied.
Screen readers:
- Buttons use verb-noun labels, for example "Save evidence" or "Share report".
- Icon-only controls need explicit accessibility labels.
- Consent controls announce the current state and consequence.
- Evidence and privacy controls announce whether data is local, queued, synced, hidden, or shared.
- Error messages are associated with the field or action that caused them.
Focus order:
- Default order is top-to-bottom, left-to-right.
- Keep related evidence, consent, and privacy controls grouped in the tree.
- Dialog focus starts on the title or first actionable control, then remains inside the dialog until dismissed.
Dynamic type:
- Components should allow font scaling and expansion through at least Extra Large.
- Text cannot overlap previous or next content at larger sizes.
- Chips and list rows wrap instead of shrinking below readable sizes.
- Critical legal or safety copy should not be hidden behind ellipses.
High contrast:
- Use
setTheme('highContrast')once an app setting is wired. - Do not special-case individual screens with local high-contrast colors; add missing tokens instead.
- Keep existing screens stable until their owning redesign issue starts.
- When touching a component, replace local hex values with
colors.<role>ortokens.colors[mode].<role>. - Prefer semantic state tokens over visual descriptions. Use
evidence, notblue; useprivacy, notpurple. - If a needed role is missing, add it to
ThemeColors, all theme modes, and this document before using it. - Reusable UI components should consume
useTheme()and token exports directly. Screens should pass intent, not raw colors, whenever possible. - Do not introduce new one-off shadows, radii, or spacing values unless the value is part of a documented component exception.
- Validate changed screens with light mode first, then dark and high-contrast snapshots before enabling those modes for users.
- No new raw hex colors in screen/component styles unless documented as a temporary migration target.
- Semantic states use the token pair for background and foreground.
- Text contrast meets the baseline for light mode.
- Minimum touch target is 44 x 44 pt.
- Dynamic type does not clip safety, consent, evidence, or privacy copy.
- Icon-only controls have labels.
- Focus order follows the visual and task order.
- React Native Paper components use the app provider theme.
- Dark/high-contrast assumptions are either verified or explicitly noted in the PR.