Mobile UI/UX Design System¶
A comprehensive design system for the Okta-Mobile app covering visual identity, interaction patterns, component specs, and UX guidelines. This document serves as both a human-readable design reference and an AI-actionable specification -- design-first language and rationale for human readers, with exact hex values, pixel dimensions, spring configs, and reference files so AI agents can implement features accurately.
Last updated: 2026-03-02
1. Design Philosophy¶
Four principles guide every design decision in the app:
Playful & Premium¶
Duolingo-inspired: spring animations, rounded shapes, bold identity colors. The app feels like a product people want to use, not a clinical tool they have to use. Springs give motion a physical, bouncy quality. Rounded corners soften the UI. Bold tab colors create personality.
Color as Identity¶
Each tab owns a color -- its icon, header accent, and in-tab highlights all share it. Users build spatial memory through color: green means sessions, coral means messages. This reduces cognitive load and makes navigation feel instant.
Tactile Feedback¶
Every touch produces haptic feedback. Buttons bounce with spring physics. Cards depress on press. The app feels responsive and alive under your fingers, not flat and dead.
Clarity over Decoration¶
Clean layouts, generous whitespace, clear visual hierarchy. No gradients for the sake of gradients, no decorative elements that don't serve function. Every pixel earns its place by improving readability or guiding the eye.
Hard Rules¶
These rules cause real bugs or broken UX when ignored. They are not optional.
-
Haptics on every interactive element. Every
TouchableOpacity, pressable card, and tappable icon fires a haptic. Missing haptic = broken UX. UseImpactFeedbackStyle.Lightfor casual touches,.Mediumfor significant actions,NotificationFeedbackType.Successfor completions. -
Tab screen content clears the floating tab bar. Every
ScrollViewon a tab screen usesuseTabBarBottomPadding()for bottom content padding. Without it, the last item hides behind the tab bar. -
All user-facing text goes through
t(). No hardcoded strings. Every visible word uses a localization key, and that key exists in both EN and RU locale files. See mobile-translations.md. -
Data-fetching screens implement all four states: loading, content, empty, error. First load shows a skeleton (not a blank screen). Errors show
ErrorStatewith a retry action. Empty data shows a helpful message with guidance (not "No data found"). Success actions showSuccessToast. -
Colors come from the palette only. No ad-hoc hex values. Every color used in a new feature must appear in the Color Palette tables below. If a color isn't listed, it doesn't belong in the app.
-
Safe areas are always handled. Use
useSafeAreaInsets()for top (notch/Dynamic Island) and bottom (home indicator). Screens that ignore safe areas break on modern devices. -
New code uses canonical token values. Typography sizes, border radii, shadow levels, and spring configs each have defined defaults below. Use the canonical default; ad-hoc values (e.g.,
fontSize: 17,borderRadius: 15,damping: 14) are not allowed.
2. Visual Identity¶
2.1 Color Palette¶
Colors are organized by intent. Every color in the app has a semantic purpose -- never pick a hex value because it "looks nice."
Brand Color¶
The tenant-configurable primary color used for sign-in buttons, focused input borders, and branded CTAs.
| Token | Value | Usage |
|---|---|---|
| Brand primary | #0066CC |
Sign-in button, input focus border, branded actions |
Configured in lib/tenantConfig.ts via primaryColor. All tenants currently use #0066CC.
Tab Identity Colors¶
Each tab owns a color that persists across its icon, header tint, and in-tab accents.
| Tab | Name | Hex | 15% Tint | Used For |
|---|---|---|---|---|
| Dashboard (Sessions) | Emerald | #10B981 |
rgba(16,185,129,0.15) |
Tab icon, pill bg, workout accents |
| Messages | Coral | #FF6B6B |
rgba(255,107,107,0.15) |
Tab icon, pill bg, message accents |
| History | Gold | #FFC800 |
rgba(255,200,0,0.15) |
Tab icon, pill bg, streak number |
| Profile | Purple | #A560E8 |
rgba(165,96,232,0.15) |
Tab icon, pill bg, settings accents |
History uses a separate headerColor: #E6B800 for header tinting (the raw gold is too bright on white).
Reference: components/navigation/tabBarConfig.ts -- TAB_CONFIGS array.
Semantic Colors¶
| Intent | Hex | Usage |
|---|---|---|
| Action / success | #10B981 |
Start button, success toast bg, progress fill, done states |
| AI accent | #667EEA |
Loading spinner bg, retry button, action links, expand text |
| Warning | #F59E0B |
Streak number, warning icon, caution states |
| Error | #EF4444 |
Error state icon bg, error toast bg, destructive buttons, missed days |
Neutral Scale¶
| Token | Hex | Usage |
|---|---|---|
| Screen background | #F8FAFC |
Full-screen background (login, loading, error) |
| Card surface | #FFFFFF |
Cards, modals, tab bar, header |
| Border (default) | #E5E7EB |
Input borders (unfocused), date circles, skeleton placeholders |
| Border (subtle) | #E2E8F0 |
Input borders on login, toggle container |
| Input background | #F3F4F6 |
Icon containers, metadata badges, description bg |
| Surface subtle | #F9FAFB |
Description section bg, loading state bg, error state bg |
| Text primary | #111827 |
Headings, card titles, bold labels |
| Text secondary | #374151 |
Body text, subtitles, exercise names |
| Text muted | #6B7280 |
Captions, descriptions, secondary labels |
| Text placeholder | #9CA3AF |
Placeholder text, day labels, rest day counts |
| Icon inactive | #64748B |
Input icons (unfocused), back button, toggle text |
| Inactive / disabled | #94A3B8 |
Inactive tab icon, disabled button bg |
Do: Use semantic colors by intent -- action green for positive actions, error red for destructive ones. Don't: Pick a color because it looks nice. Every color has a meaning.
2.2 Typography¶
A 9-tier hierarchy covers every text need. System fonts only -- San Francisco on iOS, Roboto on Android. Monospace for AI-generated content: Menlo (iOS), monospace (Android).
| Tier | Default | Allowed | Weight | Usage |
|---|---|---|---|---|
| Screen hero | 32px | 36px (brand emphasis) | bold | Login title, tenant brand name |
| Screen title | 30px | -- | 700 | Tab screen headers (via HEADER_TITLE_STYLE) |
| Feature title | 22px | 28px (section hero) | bold | Modal titles, streak number |
| Section header | 18px | -- | bold | SectionHeader title, card group labels |
| Card title | 16px | 18px (primary CTA label) | 600 | Card headings, button text, sign-in button |
| Body | 14px | 16px (loading message) | 500 | Descriptions, subtitles, toast messages |
| Input label | 14px | -- | 600 | Form field labels, toggle text, action links |
| Input text | 16px | -- | 500 | Text inside input fields |
| Caption | 12px | 11px (day labels), 13px (date text) | 600 | Exercise meta, rest day counts, timestamps |
Reference: HEADER_TITLE_STYLE in components/navigation/tabBarConfig.ts -- fontSize: 30, fontWeight: "700", letterSpacing: 0.3.
Do: Use the established size tiers. If something feels too small, step up one tier. Don't: Invent a new font size -- find the closest tier.
2.3 Iconography¶
All icons come from the Ionicons library (@expo/vector-icons).
Variant convention:
- Filled variant = active, selected, or emphasized (e.g., heart for active Dashboard tab)
- Outline variant = inactive, default, or de-emphasized (e.g., heart-outline for inactive tab)
Size tiers:
| Size | Usage |
|---|---|
| 26px | Navigation tab icons |
| 20px | Inline icons (inputs, section headers, action buttons) |
| 24px | Toast icons |
| 28px | Header action buttons |
| 32px | Modal/alert hero icons (40px for ErrorState) |
| 64px | Empty state illustrations |
Do: Use filled icons to indicate selection. Use outline icons for default/inactive states. Don't: Mix icon libraries. Stick to Ionicons throughout the app.
3. Spacing & Rhythm¶
3.1 Spacing Scale¶
| Context | Value | Notes |
|---|---|---|
| Screen horizontal padding (tab screens) | 16px | Cards, section headers, content |
| Screen horizontal padding (auth/form screens) | 30px | Login, onboarding, form-heavy screens |
| Component vertical margins | 12px | 8px (tight, e.g., section header to content), 16px (loose, e.g., between card groups) |
| Card internal padding | 16px | Standard for all card types |
| Card margin between items | 12px | marginBottom: 12 between workout cards |
| Card horizontal margin | 16px | marginHorizontal: 16 |
| List item gaps | 8px | Exercise preview rows, day columns |
| Button vertical padding (secondary) | 14px | Secondary/action buttons |
| Button vertical padding (primary CTA) | 18px | Sign-in button, main CTAs |
| Form field vertical margin | 24px | marginBottom: 24 between input wrappers |
| Contact method to inputs gap | 32px | marginBottom: 32 on toggle container |
| Modal internal padding | 24px | Centered dialog padding |
3.2 Screen Layouts¶
Three templates cover every screen in the app.
Scrollable Tab Screen¶
The default for Dashboard, Messages, History, and Profile.
+---------------------------+
| Header (white bg, |
| left-aligned title 30px) |
+---------------------------+
| |
| ScrollView |
| +---------------------+ |
| | Card | |
| +---------------------+ |
| +---------------------+ |
| | Card | |
| +---------------------+ |
| ... |
| (bottom padding for |
| tab bar clearance) |
| |
| +======================+ |
| || Floating Tab Bar || |
| +======================+ |
+---------------------------+
- Header: white background, title from
HEADER_TITLE_STYLE(30px, weight 700) - Content:
ScrollViewwithRefreshControlfor pull-to-refresh - Bottom: padding via
useTabBarBottomPadding()so content clears the floating tab bar
Form Screen¶
Used for login, onboarding, and data entry flows.
+---------------------------+
| Safe Area Top |
+---------------------------+
| Title (32px bold) |
| Subtitle (20px, #374151) |
| |
| [icon] [Email input ] |
| [icon] [Password ][eye] |
| |
| [ Primary Button ] |
| [ Secondary Link ] |
| |
| (keyboard pushes |
| content up via |
| PlatformKeyboardScroll) |
+---------------------------+
- Horizontal padding: 30px (
paddingHorizontal: 30) - Background:
#F8FAFC - Keyboard: uses
PlatformKeyboardScroll+KeyboardToolbarfor platform-correct behavior - Reference:
app/login.tsx
Full-Screen Feature¶
Used for workout execution, video playback, and immersive flows.
+---------------------------+
| Custom Header / Back |
+---------------------------+
| |
| Immersive Content |
| (no tab bar visible) |
| |
| |
+---------------------------+
| Floating Bottom Button |
| (above safe area) |
+---------------------------+
- Tab bar is hidden for full-screen features
- Bottom actions float above the safe area inset
- Custom header with back navigation
3.3 The Floating Tab Bar¶
Content on every tab screen must clear the floating tab bar. The bar hovers above the bottom safe area, so scrollable content needs bottom padding to prevent the last item from hiding behind it.
Clearance formula:
Use the useTabBarBottomPadding() hook from components/navigation/tabBarConfig.ts -- it computes the exact value accounting for device-specific safe area insets.
4. Shape Language¶
4.1 Border Radius Scale¶
A progressive scale from sharp utility shapes to full circles.
| Shape | Default | Allowed | Used For |
|---|---|---|---|
| Sharp | 8px | 4px (small badges) | Badges, chips, exercise meta containers, skeleton blocks |
| Soft | 12px | 16px (input fields) | Buttons, toggle segments, action cards |
| Round | 20px | 16px (PlanManagementCard) |
Cards (WorkoutCard), modal content |
| Modal | 20px | 24px (bottom sheet top corners) | Centered dialogs |
| Pill | 32px | -- | Floating tab bar |
| Circle | 50% | -- | Avatars, date dots (19px on 38px), icon containers, loading spinner |
4.2 Shadows & Depth¶
Four elevation levels create visual hierarchy. Always use platform-specific values.
| Level | iOS | Android | Used For |
|---|---|---|---|
| Subtle | shadowOpacity: 0.05, offset {0,2}, radius 4 |
elevation: 2 |
Input fields, back button, small cards |
| Standard | shadowOpacity: 0.15, offset {0,4}, radius 6 |
elevation: 3 |
Workout cards, action buttons, toggle active |
| Prominent | shadowOpacity: 0.25, offset {0,10}, radius 20 |
elevation: 10 |
Modals, tab bar, logo container |
| Colored glow | shadowColor: brand, opacity: 0.3, offset {0,4}, radius 8 |
elevation: 8 |
Sign-in button, loading spinner, retry button, success toast |
Allowed variant: colored glow with offset: {0,8}, radius: 16 for primary CTA buttons that need stronger emphasis (e.g., sign-in button in login.tsx).
Examples from the codebase:
- Subtle: login input field container (inputFieldContainer style in login.tsx)
- Standard: workout card outer container (container style in WorkoutCard.tsx)
- Prominent: modal content card (modalContent style in OutstandingWorkoutModal.tsx)
- Colored glow: sign-in button (signInButton style in login.tsx)
5. Component Design¶
Each component section covers: visual anatomy, states, behavior, exact specs, reference file, and do/don't.
5.1 Cards¶
Anatomy: White surface #FFFFFF, rounded corners, internal padding 16px, platform shadow.
Variants:
| Variant | Radius | Shadow | Example |
|---|---|---|---|
| Standard card | 20px | shadowOpacity: 0.15, radius: 6 / elevation: 3 |
WorkoutCard |
| Action card | 16px | shadowOpacity: 0.10, radius: 3 / elevation: 2 |
PlanManagementCard |
States:
- Default: white bg, shadow, normal scale
- Pressed: scale to 0.98 via spring (damping: 10), haptic feedback
- Loading: skeleton pulse (see Progress Indicators)
Reference: components/WorkoutCard.tsx, components/PlanManagementCard.tsx
Do: Apply shadow to the outer container, overflow hidden on the inner card. Don't: Nest cards inside cards. Keep the hierarchy flat.
5.2 Buttons¶
Decision table:
| Intent | Background | Text | Border | Example |
|---|---|---|---|---|
| Primary action | Tenant primary #0066CC |
White | None | Sign in, Submit, Save |
| Success action | #10B981 |
White | None | Start workout, Continue |
| Secondary | rgba(action,0.1) |
Action color | 1px colored | Start button (non-today) |
| Destructive | #EF4444 |
White | None | Delete, Remove, Start new plan |
| Disabled / loading | #94A3B8 |
White | None | Any button while loading |
| Ghost / text link | Transparent | #667EEA |
None | "View all", "Tap to expand", forgot password |
Behavior:
- Press: scale to 0.98 with spring (damping: 10), spring back to 1.0, fire haptic
- Loading: spinner replaces label text, button dimensions stay constant (no layout shift)
- Disabled: backgroundColor: #94A3B8, reduced shadow, disabled prop set
Specs:
- Primary CTA: paddingVertical: 18, borderRadius: 16, colored shadow glow
- Secondary: paddingVertical: 14, borderRadius: 12
- Text: 16px weight 600 (default), 18px bold (primary CTA)
- Always include gap: 8 between icon and text when both present
Reference: app/login.tsx (primary CTA), components/WorkoutCard.tsx (secondary), components/PlanManagementCard.tsx (action)
Helpers: Use createPressHandlers(scaleAnim) from lib/designSystem.ts for consistent press feedback. It returns onPressIn/onPressOut handlers that spring-animate the provided Animated.Value between 1.0 and 0.97.
Do: Keep button text to 1-3 words. Use the correct type for the intent. Don't: Use a ghost button for primary actions. Primary actions need visual weight.
5.3 Input Fields¶
Anatomy: 56px height, 16px border radius, 2px border, icon left (20px), optional action button right.
States:
| State | Border | Icon Color | Shadow |
|---|---|---|---|
| Empty | #E2E8F0 |
#64748B |
Subtle |
| Focused / filled | Tenant primary #0066CC |
Tenant primary | Colored glow opacity: 0.1 |
| Error | #EF4444 |
#EF4444 |
Red glow |
Specs:
- Container: flexDirection: "row", alignItems: "center", paddingHorizontal: 16
- Background: white
- Label: 14px weight 600, #374151, positioned above the field with 8px bottom margin
- Input text: 16px weight 500, #1E293B
- Placeholder: #94A3B8
- Icon: 20px, marginRight: 12
- Password: always include eye toggle button (eye-outline / eye-off-outline, 20px)
Reference: app/login.tsx (inputFieldContainer and inputFocused styles)
Do: Use label above the field, not placeholder-as-label. Always show the focused border color change. Don't: Skip the focused state. Users need to know which field is active.
5.4 Modals & Overlays¶
Decision table:
| When to Use | Type | Visual |
|---|---|---|
| Confirmation, alert, simple yes/no | Centered dialog | animationType: "fade", white card, 20px radius, centered on backdrop |
| Selection list, action menu, picker | Bottom sheet | Slides up from bottom, top corners 24px radius, maxHeight 80% |
| Complex multi-step flow | Full-page sheet | pageSheet presentation, step transitions with slide animation |
Backdrop: rgba(0,0,0,0.5) -- always tap-to-dismiss on backdrop press.
Centered dialog specs:
- borderRadius: 20, padding: 24, maxWidth: 400
- Shadow: shadowOpacity: 0.25, shadowRadius: 20, elevation: 10
- Hero icon at top (32px, in a colored circle container)
- Title: 22px bold, centered
- Buttons stacked vertically with 12px gap
Android: Handle hardware back button to close modals explicitly.
Reference: components/OutstandingWorkoutModal.tsx (centered dialog), components/workout/ExercisesListModal.tsx (bottom sheet/overlay)
5.5 Lists & Content Feeds¶
- Tab screen lists:
ScrollViewwithuseTabBarBottomPadding()bottom padding +RefreshControlfor pull-to-refresh - Long data lists:
FlatListfor performance -- never nest aFlatListinside aScrollView - Item gaps: 8px between exercise rows, 12px between cards
- Item separators: 1px border
#F3F4F6for flat list items
5.6 Progress Indicators¶
Loading state decision tree:
| Situation | Show | Why |
|---|---|---|
| First page load (no data yet) | Skeleton screen | User sees content shape, feels faster |
| Pull-to-refresh / re-fetch | Refresh spinner (RefreshControl) |
User initiated, expects brief wait |
| Button action (submit, save) | Inline text change in button | Button shows it's working, no layout shift |
| Known progress (upload, workout) | Progress bar | User sees how far along they are |
| Full-screen data load | LoadingState component |
Animated spinning dots, centered |
| AI streaming (inline) | AIAnswerStreamingModal |
Animated spinner + rotating status messages + progress bar (no raw data shown) |
Skeleton specs:
- Placeholder color: #E5E7EB
- Opacity pulses: 0.45 to 0.85, 700ms per direction (total cycle 1400ms)
- Shape: rounded rectangles matching the content they replace (e.g., 130x30 for title, 38x38 circle for date dots)
LoadingState specs:
- Centered on #F9FAFB background
- 8 white dots arranged in a circle (80px container, 10px dots for medium size)
- Container: #667EEA background, circular, with colored glow shadow
- Rotation: 2000ms per revolution, linear easing, infinite repeat
- Entrance: scale 0.8 to 1.0 (600ms), opacity 0 to 1 (800ms)
- Message: 16px weight 500, #6B7280
Progress bar specs:
- Height: 4px (default), 8px (prominent), border radius: 4px
- Track: #E5E7EB
- Fill: #10B981, animated with timing
AIAnswerStreamingModal specs:
- Full-screen inline overlay on #F8FAFC background (replaces the previous modal approach)
- Animated spinner: reuses LoadingState pattern -- 8 rotating white dots in #667EEA circle with colored glow
- Rotating status messages: cycles every ~4s through t("aiStreaming.analyzing"), t("aiStreaming.reviewing"), t("aiStreaming.designing"), t("aiStreaming.almostThere") -- 16px weight 500, #6B7280
- Progress bar: tracks streamingProgress from useStreamingRequest -- 4px height, #E5E7EB track, #10B981 fill, 4px radius
- Streamed content (raw JSON) is accumulated internally for the onComplete callback but never displayed to the user
- Entrance: opacity 0→1 (300ms), spinner scales 0.8→1.0 with spring
Reference: components/LoadingState.tsx, components/aiinteraction/AIAnswerStreamingModal.tsx, components/navigation/StreakHeaderWidget.tsx (skeleton loading)
5.7 Toggle / Segmented Controls¶
Container: gray pill #E2E8F0, 16px border radius, 4px internal padding.
Active segment:
- Background: tenant primary color (#0066CC)
- Text: white, 14px weight 600
- Border radius: 12px
- Shadow: colored glow (shadowColor: primary, shadowOpacity: 0.3, radius: 8, elevation: 4)
Inactive segment:
- Background: transparent
- Text: #64748B, 14px weight 600
Layout: flexDirection: "row", each segment flex: 1, paddingVertical: 14, centered text + icon (18px) with 8px gap.
Reference: app/login.tsx (contactMethodToggle, toggleOption, and toggleActive styles)
5.8 Section Headers¶
Title: 18px bold #111827, with optional icon (20px, #6B7280 default, configurable).
Optional subtitle: 14px #6B7280, 2px top margin.
Optional action link: right-aligned, #667EEA text (14px weight 500), chevron-forward icon (16px), row layout with 4px gap.
Container: flexDirection: "row", justifyContent: "space-between", paddingHorizontal: 16, paddingVertical: 12, marginBottom: 8.
Reference: components/SectionHeader.tsx
5.9 Toast Notifications¶
Position: absolute, top: 60, left: 20, right: 20, zIndex: 1000.
Background: #10B981 for success (error variant uses #EF4444).
Content: row layout -- checkmark icon (24px white) + message text (14px weight 500, white).
Border radius: 12px.
Animation:
- Show: translateY from -100 to 0 (spring, damping: 15), scale from 0.8 to 1.0 (spring, damping: 12), opacity 0 to 1 (timing, 300ms)
- Hide: reverse with timing (300ms each), then call onHide
- Auto-dismiss: 3000ms default
Shadow: colored glow -- shadowColor: #10B981, offset: {0,4}, opacity: 0.3, radius: 8.
Haptic: Haptics.notificationAsync(Success) on show.
Reference: components/SuccessToast.tsx
5.10 Empty States¶
Layout: centered in scroll area.
Anatomy:
- Icon: Ionicons, 64px, muted gray or tab accent color
- Title: 18px bold #111827, centered
- Subtitle: 14px #6B7280, centered, helpful message that guides to action
- Optional CTA button below
Do: Write empty states that guide the user toward action ("No workouts yet -- your therapist will assign your first plan"). Don't: Show a blank screen or generic "No data found."
5.11 Error States¶
Icon container: 80x80px circle, #EF4444 background, 40px border radius, with colored glow shadow (shadowColor: #EF4444, opacity: 0.3, radius: 8, elevation: 8).
Icon: warning, 40px, white.
Title: 20px bold #111827, centered, 8px bottom margin.
Message: 14px #6B7280, centered, lineHeight: 20, 24px bottom margin.
Animated entrance: scale 0.8 to 1.0 (spring, damping: 15), opacity 0 to 1 (timing, 600ms).
Retry button: #667EEA background, pill shape (25px radius), row layout with refresh icon + text, paddingHorizontal: 24, paddingVertical: 12. Spring bounce on press (scale to 0.95 then back, damping: 10).
Reference: components/ErrorState.tsx
Do: Always offer a retry action. Show a friendly title + helpful message. Don't: Show raw error messages to users. Catch and humanize every error.
6. Motion & Animation¶
6.1 Animation Principles¶
- Springs over linear timing -- things feel physical, not mechanical. Springs have mass and bounce; linear feels robotic.
- Every animation serves a purpose: feedback (press response), transition (screen/tab change), or delight (completion celebration).
- Faster for feedback (150-300ms), slower for decoration (600-800ms). The user should never wait for an animation to finish before they can act.
6.2 Motion Vocabulary¶
| Motion | Description | When |
|---|---|---|
| Press feedback | Scale down to 0.98, spring back to 1.0 | Card tap, button press |
| Tab transition | Colored pill slides with spring, icon scales up to 1.15x | Tab switch |
| Screen entry | Fade + directional slide (FadeInRight/Left) | Step flows, navigation |
| Loading to content | Skeleton pulse stops, real content fades in | Data arrives |
| Toast | Slides down from top with bounce, auto-slides out | Success/error notification |
| Progress fill | Smooth timing animation left-to-right | Upload, workout progress |
| Entrance pop | Scale from 0.8 to 1.0 with spring | Error state, modal content |
6.3 Spring Configs¶
Exact values from the codebase. Use these -- don't invent new spring parameters.
| Name | Damping | Stiffness | Feel | Used For |
|---|---|---|---|---|
| Smooth slide | 18 | 200 | Smooth, no overshoot | Tab pill translateX, color interpolation |
| Snappy scale | 12 | 250 | Quick with slight bounce | Icon scale on tab switch |
| Gentle press | 10 | (default) | Soft cushion | Button/card press feedback |
| Entrance bounce | 15 | (default) | Welcoming pop | Error state entrance, toast show |
| Toast scale | 12 | (default) | Quick pop-in | Toast scale animation |
Reference: components/navigation/tabBarConfig.ts -- PILL_SLIDE_SPRING, ICON_SCALE_SPRING, COLOR_SPRING.
6.4 Timing Guidelines¶
| Duration | Category | Examples |
|---|---|---|
| 150-300ms | Fast feedback | Opacity fade (toast show/hide), backdrop appear |
| 300-600ms | Content transition | Error state opacity fade-in (600ms) |
| 600-800ms | Decorative entrance | Loading state opacity (800ms), scale entrance (600ms) |
| 700ms | Skeleton pulse | One direction of the pulse cycle (0.45 to 0.85 or back) |
| 1000ms | Progress fill | Upload bar, workout progress animation |
| 2000ms | Continuous loop | Loading spinner rotation (one full revolution) |
7. Haptic Design¶
| Interaction | Haptic Type | Examples |
|---|---|---|
| Casual touch | ImpactFeedbackStyle.Light |
Tab switch, card press, list item tap, thumbnail press |
| Significant action | ImpactFeedbackStyle.Medium |
Plan management press, form submit, start workout |
| Completion | NotificationFeedbackType.Success |
Workout done, upload complete, success toast |
Rule: Every interactive element fires a haptic. A tap with no haptic response = broken UX. Test haptics manually on a physical device -- they don't fire in simulators.
Helpers: Use the shared helpers from lib/designSystem.ts instead of calling expo-haptics directly:
- hapticLight() — wraps Haptics.impactAsync(ImpactFeedbackStyle.Light)
- hapticMedium() — wraps Haptics.impactAsync(ImpactFeedbackStyle.Medium)
- hapticSuccess() — wraps Haptics.notificationAsync(NotificationFeedbackType.Success)
Do: Fire haptics on every interactive element. Use Light for casual browsing, Medium for significant actions. Don't: Use Medium/Heavy for casual browsing. Reserve stronger haptics for moments that matter.
8. Accessibility¶
| Concern | Requirement |
|---|---|
| Touch targets | Minimum 44x44pt for all interactive elements |
| Press feedback | activeOpacity: 0.8 on all TouchableOpacity (or use Reanimated scale) |
| Screen reader labels | accessibilityLabel on all interactive elements, especially icon-only buttons |
| Selection state | Announce active/selected state to screen readers (tab bar, toggles) |
| Disabled state | Use disabled prop (visual + functional), not just opacity change |
| Text overflow | numberOfLines with expand affordance for long content |
9. Platform Considerations¶
Shadows¶
iOS uses shadowColor, shadowOffset, shadowOpacity, shadowRadius. Android uses elevation. Always provide both via Platform.select or by setting both properties (RN applies the correct one per platform).
Safe Areas¶
Always use useSafeAreaInsets() from react-native-safe-area-context for:
- Top: notch / Dynamic Island clearance
- Bottom: home indicator / navigation bar clearance
Keyboard¶
See mobile-keyboard-handling.md for the full platform-by-platform keyboard architecture, including PlatformKeyboardScroll and KeyboardToolbar usage.
Back Navigation¶
- iOS: swipe-back gesture is handled natively by React Navigation
- Android: hardware back button must be explicitly handled for overlays and modals (close the overlay, don't navigate back)
10. UX Patterns & Flows¶
10.1 Error Recovery¶
| Error Context | How It Surfaces | Recovery |
|---|---|---|
| Initial page load fails | Full-screen ErrorState |
Retry button reloads data |
| Action fails (submit, save) | Alert dialog or inline error | User can retry the action |
| Network offline | Toast notification | Auto-retry when connection returns |
| Partial data failure | Graceful degradation | Show what loaded, hide what didn't |
10.2 Role-Based Experience¶
The app supports two roles with different navigation structures:
- Patient: 4 tabs -- Dashboard (Sessions), Messages, History, Profile
- Doctor: 3 tabs -- Dashboard, Messages, Profile (History is hidden)
Doctor mode hides default headers; screens manage their own top padding. The tab bar dynamically adjusts pill width based on visible tab count.
See mobile-role-separation.md for the full route separation architecture, RoleGuard, and file structure conventions.
11. New Feature Design Checklist¶
Work through this list in order when building any new screen or feature.
Setup¶
- [ ] Which role? Patient, doctor, or both?
- [ ] Which screen template? Scrollable tab / form / full-screen feature?
- [ ] Which tab does it belong to? (determines accent color)
Layout¶
- [ ] Screen padding matches the spacing scale (16px tab, 30px form)
- [ ] Safe areas handled (top and bottom)
- [ ] Scrollable content has bottom padding for floating tab bar (
useTabBarBottomPadding()) - [ ] ASCII wireframe sketched before building
Visual Design¶
- [ ] All colors from the semantic palette -- no ad-hoc hex values
- [ ] Typography follows the 9-tier hierarchy
- [ ] Border radii from the radius scale
- [ ] Shadow level matches component importance
- [ ] Icons are Ionicons, correct variant (filled/outline) and size tier
Interactions¶
- [ ] Buttons use the correct type for their intent (primary/secondary/destructive/ghost)
- [ ] Press feedback: use
createPressHandlers()fromlib/designSystem.tsfor spring bounce - [ ] Haptics: use
hapticLight()/hapticMedium()/hapticSuccess()fromlib/designSystem.ts - [ ] Haptic level matches interaction importance (light/medium/success)
- [ ] Input fields have all states: empty, focused, filled, error
States¶
- [ ] Loading: skeleton for first load, spinner for actions, progress bar for determinate
- [ ] Empty state: helpful message + icon + optional CTA (not blank screen)
- [ ] Error state: clear message + retry action (not raw error text)
- [ ] Success: toast notification for completions
Content¶
- [ ] All user-facing text through
t()localization -- no hardcoded strings - [ ] Keys added to both EN and RU locale files
- [ ] Text truncated with
numberOfLineswhere overflow is possible
Accessibility¶
- [ ] Touch targets minimum 44x44pt
- [ ] Screen reader labels on icon-only buttons
- [ ] Disabled buttons use
disabledprop - [ ]
activeOpacity: 0.8on allTouchableOpacity
Polish¶
- [ ] Animations use springs (not linear timing) for interactive feedback
- [ ] Spring configs from the reference table (damping/stiffness values)
- [ ] Haptics fire on every interactive element (test on physical device)
- [ ] Test on both iOS and Android
- [ ] Test with both EN and RU language
12. Key Reference Files¶
| File | What It Defines |
|---|---|
components/navigation/tabBarConfig.ts |
Tab colors, dimensions, spring configs, useTabBarBottomPadding() |
lib/tenantConfig.ts |
Tenant brand color, name, logo, app type |
components/LoadingState.tsx |
Full-screen animated loading pattern |
components/ErrorState.tsx |
Full-screen error with animated entrance and retry |
components/SuccessToast.tsx |
Toast notification with spring animation and haptic |
components/SectionHeader.tsx |
Section header with optional icon, subtitle, action link |
components/WorkoutCard.tsx |
Card pattern with press feedback, skeleton, thumbnails |
components/PlanManagementCard.tsx |
Action card pattern with haptic feedback |
lib/designSystem.ts |
Shared haptic helpers (hapticLight, hapticMedium, hapticSuccess) + spring press handler (createPressHandlers) |
components/aiinteraction/AIAnswerStreamingModal.tsx |
AI streaming loading experience -- spinner, rotating status messages, progress bar |
app/login.tsx |
Input fields, buttons, segmented control, form layout |
components/workout/PatientFeedbackModal.tsx |
Multi-step modal with step transitions |
components/OutstandingWorkoutModal.tsx |
Centered confirmation dialog pattern |
components/workout/ExercisesListModal.tsx |
Bottom sheet / overlay modal pattern |
components/navigation/StreakHeaderWidget.tsx |
Skeleton loading, streak visualization, day dots |
All paths relative to Okta-Mobile/.
13. Related Documentation¶
| Document | Covers |
|---|---|
| mobile-navigation-design.md | Floating tab bar, headers, StreakHeaderWidget -- full navigation spec |
| mobile-keyboard-handling.md | Keyboard architecture, platform-specific handling, component decision matrix |
| mobile-role-separation.md | Patient/doctor route separation, RoleGuard, file structure |
| mobile-workout-flow.md | Workout screen flow, HUD variants, upload queue |
| mobile-translations.md | Localization guide, key naming, variable interpolation |
| feature-flags.md | Feature flag system for tenant-level feature gating |
| streaks.md | Streak data model, API, frontend rendering |
14. Future Considerations¶
- Design Tokens File: All colors are manually matched today. A central
tokens.tsfile would prevent drift across components and enable runtime theming. - Dark Mode: Every color is hardcoded for light mode. A dark mode pass needs updated backgrounds, card surfaces, text colors, and contrast verification against WCAG AA.
- Shared Component Library:
LoadingState,ErrorState,SuccessToast,SectionHeaderare good candidates for formalization into a shared component package with typed props and Storybook stories. - RTL Support: EN and RU are both LTR. Adding Hebrew, Arabic, or other RTL languages would require layout direction work across all screens.