Appearance
Accessibility and Dark Mode make every mobile flow readable, operable, and theme-aware from day one.
The V1 baseline should treat accessibility and theme support as app infrastructure, not cleanup work after screens are built. Coach mobile and client mobile share tokens, preference resolution, native accessibility helpers, QA checks, and component contracts while still allowing each product surface to tune density, navigation, and information priority.
Tags: Mobile Experience · Expo · Shared tokens · WCAG AA · User preference
Boundary decision
this feature owns cross-mobile accessibility standards, token generation, theme preference resolution, and QA gates. Individual feature screens still own their domain content, validation rules, and workflow-specific empty, loading, error, offline, and conflict states.
WCAG 2.2 AA requirement
The default requirement is WCAG 2.2 AA where applicable to native mobile UI, with explicit exceptions documented before release. Critical task flows must remain usable with screen readers, large text, reduced motion, high contrast needs, one-handed touch, and dark environments.
Accessibility And Theme Flows
Theme and accessibility behavior starts in app shell initialization and follows the user across every core mobile task.
First Launch And Preference Resolution
- Mobile shell reads device color scheme, text scale, reduce motion, bold text, screen reader state, locale, and direction before rendering the authenticated app shell.
- The app applies the saved CoachMe theme preference when present: system, light, dark, or high contrast.
- Design tokens resolve into native-safe color, spacing, radius, typography, focus, elevation, and state values for the current mode.
- The selected theme is applied before navigation mounts so login, onboarding, Today, workout logging, quick-log, chat, and settings do not flash the wrong colors.
- Anonymous users keep preference locally; authenticated users sync preference to profile settings after login.
User Changes Theme Or Accessibility Settings
- User opens settings and chooses system, light, dark, or high contrast mode.
- Mobile writes the preference to secure local settings and posts it to the profile preferences endpoint when authenticated.
- The app re-resolves tokens in memory without losing navigation state, unsaved form drafts, workout logs, or queued offline operations.
- If device accessibility settings change while the app is active, the shell updates text scaling, motion policy, and announcement behavior on the next render tick.
- Telemetry records only coarse accessibility QA signals and never stores disability, medical, or assistive-technology usage as a sensitive profile attribute.
Critical Mobile Task Completion
- Client completes Today, quick-log, workout logging, meal, form, measurement, and messaging flows with touch targets that remain usable at large text sizes.
- Screen readers expose headings, labels, roles, hints, selected state, disabled state, required fields, error summaries, offline status, and accepted or queued sync state.
- Color is never the only way to communicate completion, risk, conflict, pain, missed status, validation error, or destructive action.
- Motion-heavy feedback, skeletons, timers, progress rings, and chart animations respect reduce motion.
- Dark mode preserves contrast for coach review, client logging, charts, media overlays, inputs, and destructive or warning states.
Coach Review And Dense Workflows
- Coach mobile keeps dashboards, task inbox, client profile summaries, forms, program editing, progress charts, and messaging readable in light and dark mode.
- Compact review cards expose accessible summaries instead of forcing screen-reader users through every decorative metric one by one.
- Charts include text summaries, legends with non-color indicators, touch-accessible data points, and fallback tables for important comparisons.
- Drag-and-drop or reorderable controls provide explicit move up, move down, and position actions for assistive technologies.
- High-density coach screens preserve minimum interactive sizes even when visual layout uses compact spacing.
Domain Model
This is a shared mobile foundation feature with profile-level preferences and platform-specific runtime adapters.
Aggregates & Entities
UserExperiencePreference: user-owned preference record for theme mode, contrast mode, motion preference override, text density, and locale-direction defaults.DesignTokenSet: versioned build artifact defining semantic colors, typography, spacing, border, elevation, focus, chart, and status tokens.AccessibilityAuditFinding: QA record for a screen, flow, component, token, severity, owner, status, and release gate decision.MobileAccessibilityAdapter: runtime adapter that reads platform accessibility state and exposes normalized helpers to shared mobile components.ThemeResolver: application service that combines user preference, system settings, token version, locale direction, and feature flags into the active theme context.
Value Objects & Rules
ThemeMode:SYSTEM,LIGHT,DARK,HIGH_CONTRAST.ContrastTarget: minimum contrast rules for text, icon-only controls, borders, focus indicators, charts, and disabled states.AccessibilityState: screen reader enabled, reduce motion, bold text, text scale, invert colors, grayscale, high contrast, and platform source.SemanticColorRole: background, surface, text, muted text, action, success, warning, danger, info, offline, syncing, accepted, and conflicted.- Theme tokens must be semantic; feature code must not hardcode raw light or dark colors outside token definitions.
- Every interactive component must expose name, role, state, disabled reason when useful, and a deterministic focus order.
- Validation, sync, conflict, warning, and success states must combine text, iconography, shape, or position with color.
| Model | Owned By | Important Methods | Emits |
|---|---|---|---|
UserExperiencePreference | IdentityAccess profile settings | setThemeMode(), setMotionOverride(), resolveDefaults(), sanitizeForSync() | UserExperiencePreferenceChanged |
DesignTokenSet | Shared UI packages | validateContrast(), compileForNative(), compileForWeb(), diffVersion() | DesignTokenSetPublished |
AccessibilityAuditFinding | Quality workflow | open(), assign(), waive(), resolve(), blocksRelease() | AccessibilityFindingOpened, AccessibilityFindingResolved |
ThemeResolver | packages/mobile-core | resolve(), subscribeToSystemChanges(), hydratePreference(), emitTokenSnapshot() | Runtime theme change notification. |
Design Token Contract
Mobile apps consume semantic tokens compiled from one source of truth and validated before release.
| Token Area | Contract | Validation | Consumers |
|---|---|---|---|
| Color | Semantic roles for background, surface, text, border, action, status, chart, and overlay states in light, dark, and high contrast. | Automated contrast checks for text, icons, focus indicators, charts, disabled controls, warning/danger states, and selected states. | packages/ui-mobile, packages/ui-web, chart components, notification templates. |
| Typography | Named text styles with minimum size, line height, weight, and dynamic type scaling caps per component density. | Large text screenshots, truncation checks, multiline label tests, Arabic/English mixed-content review. | All app shells, forms, cards, tables, navigation, charts, and modals. |
| Touch Target | Minimum interactive target size, hit slop rules, icon button dimensions, spacing around destructive controls, and one-handed thumb reach guidance. | Component tests assert minimum target boxes and no overlapping hit regions. | Buttons, quick-log controls, workout set rows, list actions, tabs, bottom sheets. |
| Focus And State | Focus ring, pressed, selected, disabled, loading, pending sync, accepted, rejected, conflict, offline, and destructive tokens. | Screen-reader and keyboard or hardware focus checks on forms, menus, tabs, and reorder controls. | Inputs, segmented controls, lists, dialogs, bottom sheets, toast/alert system. |
| Motion | Durations and animation types mapped to standard and reduced-motion variants. | Reduced-motion tests ensure no essential state is communicated only through animation. | Navigation transitions, skeletons, charts, progress indicators, completion feedback. |
Semantic token naming
Use semantic names such as color.status.danger.text and color.sync.pending.background. Raw palette names are allowed only inside token source files.
Storage
Persist user preferences centrally, cache them locally, and keep token/audit metadata versioned.
| Table Or Store | Purpose | Important Fields | Lifecycle |
|---|---|---|---|
user_experience_preferences | Canonical authenticated preference for theme and accessibility-related app choices. | id, user_id, theme_mode, contrast_mode, motion_override, text_density, last_token_version, updated_at | Soft delete only with user account lifecycle; update in place for current preference and audit through events. |
design_token_versions | Track published token bundles used by apps and QA gates. | id, version, platform, checksum, published_by_user_id, published_at, release_channel | Append-only. Older versions remain for reproducing screenshots and support issues. |
accessibility_audit_findings | Store manual and automated accessibility findings for release readiness. | id, surface, screen_key, component_key, criterion, severity, status, owner_user_id, waiver_reason, resolved_at | Append-only status transitions; waivers need explicit owner, reason, and expiry. |
| Mobile secure local settings | Anonymous or pre-hydration theme preference and last resolved accessibility state. | themeMode, lastResolvedScheme, tokenVersion, updatedAt | Overwrite current value; migrate to server preference after authenticated sync. |
| Mobile SQLite metadata | Offline-safe UI state for token version and QA diagnostics. | key, value, updated_at | Prune with app metadata migrations; never store sensitive assistive-technology details for analytics. |
API Contracts
Preference APIs are small, profile-scoped, and safe for early app shell hydration.
| Endpoint | Purpose | Request | Response |
|---|---|---|---|
GET /api/v1/me/experience-preferences | Read the authenticated user's theme and app experience preferences. | Authenticated user session, optional app version and token version headers. | Theme mode, contrast mode, motion override, text density, locale/direction hint, updated timestamp. |
PATCH /api/v1/me/experience-preferences | Update theme or accessibility-related app preferences. | Partial preference update with expected updated timestamp for optimistic concurrency. | Accepted preference record and updated timestamp. |
GET /api/v1/mobile/design-tokens/manifest | Return token bundle metadata for runtime diagnostics and release-channel validation. | App id, platform, version, release channel, current token checksum. | Current token version, checksum, minimum supported app version, deprecation notices. |
POST /api/v1/internal/quality/accessibility-findings | Ingest automated accessibility audit findings from CI or release QA tooling. | Build id, screen key, component key, criterion, severity, evidence link. | Created or deduplicated finding summary. |
GET /api/v1/admin/quality/accessibility-findings | Admin/support read model for open release-blocking findings. | Surface, severity, status, release channel filters. | Paginated findings with owners, waivers, and resolution status. |
Key Code Snippets
Implementation sketches define preference storage, token resolution, mobile providers, and component accessibility contracts.
Drizzle schema shape
apps/api/src/identity-access/db/experience-preferences.schema.ts
ts
export const userExperiencePreferences = pgTable("user_experience_preferences", {
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id").notNull(),
themeMode: text("theme_mode").$type<ThemeMode>().notNull().default("SYSTEM"),
contrastMode: text("contrast_mode").$type<ContrastMode>().notNull().default("STANDARD"),
motionOverride: text("motion_override").$type<MotionOverride>().notNull().default("SYSTEM"),
textDensity: text("text_density").$type<TextDensity>().notNull().default("STANDARD"),
lastTokenVersion: text("last_token_version"),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
deletedAt: timestamp("deleted_at", { withTimezone: true }),
}, (table) => ({
userUnique: uniqueIndex("user_experience_preferences_user_unique")
.on(table.userId)
.where(sql`${table.deletedAt} is null`),
}));Theme resolver
packages/mobile-core/src/theme/theme-resolver.ts
ts
export function resolveTheme(input: ResolveThemeInput): ResolvedTheme {
const requestedMode = input.preference.themeMode ?? "SYSTEM";
const colorScheme = requestedMode === "SYSTEM"
? input.system.colorScheme
: requestedMode.toLowerCase();
const contrast = requestedMode === "HIGH_CONTRAST" || input.system.highContrast
? "HIGH"
: "STANDARD";
return {
mode: colorScheme === "dark" ? "DARK" : "LIGHT",
contrast,
reduceMotion: input.preference.motionOverride === "REDUCE" || input.system.reduceMotion,
textScale: clampTextScale(input.system.textScale),
direction: input.locale.direction,
tokens: compileMobileTokens({ mode: colorScheme, contrast, direction: input.locale.direction }),
};
}Mobile provider shell
packages/mobile-core/src/theme/mobile-theme-provider.tsx
ts
export function MobileThemeProvider({ children }: PropsWithChildren) {
const system = useSystemAccessibilityState();
const preference = useExperiencePreferenceStore();
const locale = useLocaleDirection();
const theme = useMemo(
() => resolveTheme({ system, preference, locale }),
[system, preference, locale],
);
useEffect(() => {
setNativeNavigationTheme(theme);
setStatusBarStyle(theme.mode === "DARK" ? "light" : "dark");
}, [theme]);
return (
<ThemeContext.Provider value={theme}>
<AccessibilityAnnouncer.Provider reduceMotion={theme.reduceMotion}>
{children}
</AccessibilityAnnouncer.Provider>
</ThemeContext.Provider>
);
}Accessible action component
packages/ui-mobile/src/actions/quick-action-button.tsx
ts
export function QuickActionButton(props: QuickActionButtonProps) {
const theme = useMobileTheme();
return (
<Pressable
accessibilityRole="button"
accessibilityLabel={props.label}
accessibilityHint={props.hint}
accessibilityState={{
disabled: props.disabled,
busy: props.syncState === "SYNCING",
selected: props.selected,
}}
disabled={props.disabled}
hitSlop={theme.touch.hitSlop}
style={({ pressed }) => [
styles.base,
{
minHeight: theme.touch.minTarget,
backgroundColor: theme.tokens.color.action.background,
opacity: props.disabled ? theme.opacity.disabled : pressed ? theme.opacity.pressed : 1,
},
]}
onPress={props.onPress}
>
<Icon name={props.icon} color={theme.tokens.color.action.icon} />
<Text style={theme.text.button} numberOfLines={2}>{props.label}</Text>
</Pressable>
);
}Events & Background Jobs
Theme and accessibility changes should be lightweight, auditable, and useful to release quality workflows.
Events
UserExperiencePreferenceChanged: updates profile read models and causes mobile sessions to refresh preference snapshots.DesignTokenSetPublished: records token version, checksum, platform targets, and release channel.AccessibilityFindingOpened: creates a release-quality task for blocking or high-priority issues.AccessibilityFindingResolved: closes the quality loop and links the fix to the affected screen or component.
Jobs
- Token validation job checks contrast pairs, missing semantic roles, chart palettes, and status combinations for every supported mode.
- Screenshot QA job captures key mobile flows in light, dark, high contrast, Arabic RTL, English LTR, and large text configurations.
- Audit ingestion job deduplicates CI findings by screen key, component key, criterion, app version, and token version.
- Release gate job blocks production release when critical flows have unresolved severe findings without approved waiver.
Permissions & Access Rules
User preferences are personal profile data; audit findings are operational quality data.
User Preferences
- Authenticated users can read and update only their own experience preferences.
- Coaches cannot read a client's theme or accessibility preferences through coaching surfaces.
- Support can see current preference values only with explicit account-support permission and audited reason.
- Anonymous local preferences are device-local until the user signs in and chooses to keep or override synced preferences.
Quality Records
- Engineers, QA, design, and product admins can read accessibility findings for assigned release surfaces.
- Only authorized owners can waive severe findings, and waivers require expiry, reason, and affected release channel.
- Audit records must not store user health, nutrition, message, photo, or form content as evidence.
- Design token publishing requires elevated design-system permission and produces an append-only version record.
Web, Coach Mobile, Client Mobile
The mobile apps are the primary target, while web shares tokens and profile preference APIs where practical.
Coach Web
- Consumes the same semantic token source for light and dark mode, but can use web-specific compiled values and keyboard focus behavior.
- Settings can expose theme preference for the user account, keeping the contract aligned with mobile.
- Dense tables, builders, dashboards, and charts need text summaries, keyboard focus, non-color status indicators, and dark-mode contrast checks.
Coach Mobile
- Supports dark mode, high contrast tokens, dynamic type, screen reader labels, reduced motion, and accessible chart summaries across dashboard, inbox, profiles, programs, forms, nutrition, progress, and messaging.
- Builder and reorder flows include non-drag alternatives, explicit action labels, and confirmation for destructive changes.
- Coach mobile can use denser layouts than client mobile only when target size, focus order, and large-text behavior remain valid.
Client Mobile
- Today, quick-log, workout logging, onboarding, forms, meals, progress capture, and chat remain usable with screen readers, large text, RTL, offline banners, and queued sync announcements.
- Client-facing statuses use readable text plus icons or shape, not color alone, especially for missed tasks, pain notes, warnings, conflicts, and accepted completions.
- Dark mode is optimized for gyms and night use without reducing readability of forms, timers, media, charts, or coach messages.
Test Scenarios & Resolved Decisions
Accessibility and dark mode need automated coverage plus manual assistive-technology review for the flows that matter most.
Tests
- Token tests: all semantic light, dark, and high contrast pairs meet contrast targets for text, icons, status chips, chart marks, focus indicators, and disabled states.
- Component tests: buttons, inputs, segmented controls, tabs, cards, list rows, bottom sheets, modals, toasts, charts, and upload controls expose correct roles, labels, states, and minimum target sizes.
- Mobile E2E tests: login, onboarding, Today, quick-log, workout logging, form submission, meal logging, progress photo upload, messaging, and settings run in light, dark, RTL, large text, and reduce motion configurations.
- Screen-reader tests: VoiceOver and TalkBack can navigate critical flows with logical heading order, useful hints, announced validation errors, and offline/sync state changes.
- Regression tests: switching theme during an active workout, draft form, upload, or offline queue does not lose user input or corrupt sync state.
- Snapshot tests: compare core screens across token versions and release channels to catch unreadable color, clipped text, overlapping controls, and hidden focus states.
Resolved Decisions
- Public V1 exposes high contrast as an explicit in-app theme mode and also follows platform high-contrast settings when the user leaves theme preference on system.
- CoachMe uses Victory Native XL behind an internal accessible chart adapter for Expo mobile charts; the adapter owns chart summaries, fallback tables, RTL legend order, semantic palettes, and screen-reader labels instead of relying on the chart renderer alone.
- Coach-facing surfaces expose only standard and compact density in Public V1. Compact density may reduce whitespace and card chrome, but it cannot reduce minimum touch targets, suppress dynamic type, hide required labels, or disable screen-reader summaries.
- Every production release needs VoiceOver and TalkBack smoke sign-off for app shell, sign-in, settings/theme changes, Today, quick-log, active workout logging, form submission with validation errors, messaging, and offline/sync status. Major UI releases add full sign-off for onboarding, coach dashboards, client profiles, program editing, nutrition charts, progress media, push deep links, and any redesigned critical flow.
- Launch app-store screenshots and marketing assets include both light and dark mode. Light mode remains the default sequence, dark mode appears in at least one store screenshot per app surface, and high contrast is shown in accessibility/support material rather than primary marketing.