Skip to content

Ideony Mobile — Accessibility Audit

Date: 2026-04-20 Scope: apps/mobile/app/ (all screens) + apps/mobile/components/ (all shared components) Standard: WCAG 2.1 AA + React Native a11y API


C1. No Reduced Motion Support (WCAG 2.3.3 — Animation from Interactions, AA)

Section titled “C1. No Reduced Motion Support (WCAG 2.3.3 — Animation from Interactions, AA)”

lib/animations.ts enteringStagger and enteringFade contain zero useReducedMotion() checks. withSpring press-scale animations site-wide also unchecked. useReducedMotion() used in exactly one file: WelcomeTour.tsx.

Affected everywhere: Every screen using enteringStagger (triage, results, dashboard, requests, calendar, bookings, profile, onboarding, compare, portfolio, quotes); CompareSheet, EmptyResults, AddressAutocomplete, DateTimePicker, PriceSummary, QuoteCard, SkeletonLoader, ProgressBar, PaginationDots, Header (ZoomIn/FadeIn), ProCard (FadeInUp), BookingCard (FadeInDown), PromptCard (FadeInDown), WelcomeTour carousel.

Fix pattern: const reducedMotion = useReducedMotion(); const entering = reducedMotion ? undefined : FadeInDown…


C2. SkeletonLoader Continuous Animation — No Pause/Stop (WCAG 2.2.2)

Section titled “C2. SkeletonLoader Continuous Animation — No Pause/Stop (WCAG 2.2.2)”

/components/SkeletonLoader.tsxwithRepeat(withTiming(1, {duration:1200}), -1, false) runs indefinitely with no reduced-motion gate. Auto-playing content that moves for >5 seconds without user-controllable pause violates WCAG 2.2.2.


C3. DateTimePicker — Calendar and Time Slots Completely Inaccessible (WCAG 1.3.1, 4.1.2)

Section titled “C3. DateTimePicker — Calendar and Time Slots Completely Inaccessible (WCAG 1.3.1, 4.1.2)”

/components/booking/DateTimePicker.tsx:

  • Navigation Pressables (ChevronLeft/Right): no accessibilityRole, no accessibilityLabel, no accessibilityState={{ disabled: isPrevDisabled }}
  • Each calendar day Pressable: no accessibilityRole, accessibilityLabel = only the bare digit (e.g. “4”), no accessibilityState={{ selected, disabled }}
  • SlotChip Pressable: no accessibilityRole, no accessibilityState={{ selected }}

VoiceOver/TalkBack user cannot determine month, selected date, disabled past dates, or selected time slot. Date selection is functionally impossible via AT.


C4. Text Contrast — Sub-4.5:1 at ≤14pt (WCAG 1.4.3)

Section titled “C4. Text Contrast — Sub-4.5:1 at ≤14pt (WCAG 1.4.3)”

Systematic use of 11–13pt text with low-contrast tokens:

LocationText / BackgroundSizeRisk
IdentityStrip.tsx verifiedLabeltextSecondary on surface11ptFails
TrustTierBadge ELITElight text on dark olive11ptFails
VerifiedBadge labeltextSecondary on primarySubtle11ptFails
CredentialCard status badgecolored text on tinted bg11ptFails
Admin credential type badgecolored text on tinted bg11ptFails
CompareSheet miniNametext on surface11ptVerify
PriceSummary fine-print notetextTertiary on background11ptFails
ProgressBar labeltext at opacity: 0.512ptFails
TriageChips option texttextSecondary on surface13ptVerify

opacity: 0.5 on the ProgressBar label is a critical anti-pattern — effective color is composited, fails any contrast checker.


C5. Emergency SOS Button — No Accessibility (WCAG 4.1.2)

Section titled “C5. Emergency SOS Button — No Accessibility (WCAG 4.1.2)”

/components/home/SOSButton.tsx — The emergency dispatch control has no accessibilityRole, no accessibilityLabel, no accessibilityState. An AT user cannot discover, identify, or activate emergency services. This is the highest-severity single-element failure in the app.


C6. Rating Stars — No Role or Label (WCAG 4.1.2)

Section titled “C6. Rating Stars — No Role or Label (WCAG 4.1.2)”

/app/review/[bookingId].tsx StarButton — interactive star rating has no accessibilityRole, no accessibilityLabel (e.g. “3 stars”), no accessibilityState={{ selected }}. Review submission is inaccessible to AT.


C7. CandidateCard Uses onTouchEnd Instead of onPress (WCAG 2.1.1)

Section titled “C7. CandidateCard Uses onTouchEnd Instead of onPress (WCAG 2.1.1)”

/app/compare.tsx CandidateCard — VoiceOver/TalkBack activates via synthesized press events, not raw touch events. onTouchEnd is not triggered by AT → compare feature selection broken for blind users.


C8. Skeleton Containers Not Hidden from AT (WCAG 1.3.1)

Section titled “C8. Skeleton Containers Not Hidden from AT (WCAG 1.3.1)”

SkeletonLoader.tsx skeleton containers have no accessibilityElementsHidden={true} / importantForAccessibility="no-hide-descendants". AT reads meaningless shimmer blocks as navigable content, polluting reading order during loading states.


M1. Decorative Icons Not Hidden — App-Wide

Section titled “M1. Decorative Icons Not Hidden — App-Wide”

Lucide and Phosphor icons across every screen and component lack accessibilityElementsHidden={true}. AT announces icon element names (e.g. “chevron left”, “x circle”, “map pin”) redundantly alongside their parent labels. Affects 100% of icon usage outside WelcomeTour.tsx.

Exceptions (good): WelcomeTour.tsx correctly hides decorative icons.


M2. Missing accessibilityRole="header" on Screen and Section Titles — App-Wide

Section titled “M2. Missing accessibilityRole="header" on Screen and Section Titles — App-Wide”

No screen uses accessibilityRole="header" on its title Text. Section headers (profile, settings, dashboard stats, credential sections) also unlabeled. AT users cannot navigate by heading or understand document structure.


M3. Input.tsx Label Not Programmatically Associated

Section titled “M3. Input.tsx Label Not Programmatically Associated”

/components/ui/Input.tsx — visible label Text rendered separately with no accessibilityLabel on the TextInput. All ~30 form fields across triage, onboarding, booking, chat, profile, settings, verification inherit this failure. Raw TextInput instances in AddressAutocomplete, CredentialReviewSheet, AddCredentialSheet, PromptCard, app/quotes/new.tsx are similarly unlabeled.


M4. Action Buttons Missing Role and Label — Systemic Pattern

Section titled “M4. Action Buttons Missing Role and Label — Systemic Pattern”

All primary CTAs without accessibilityRole="button" and accessibilityLabel:

  • All 4 Pressables in CredentialReviewSheet (approve, reject, cancel, confirm-reject)
  • All 3 Pressables in CompareSheet (remove mini-card ×n, clear, compare)
  • Both Pressables in EmptyResults (clear filters, go back)
  • Both Pressables in MediaAttachments (add, remove) — have accessibilityLabel but no role
  • Back buttons on 20+ screens: padding: 4 → 32pt, no role, no label
  • Header.tsx iconButton 36×36pt: below minimums, no role
  • Settings menu items: no role, no label
  • StickyBookBar “Book” button: has role+label (positive exception)

M5. Disabled State Not in accessibilityState

Section titled “M5. Disabled State Not in accessibilityState”

Multiple interactive elements use disabled prop and visual opacity but omit accessibilityState={{ disabled: true }}:

  • CompareSheet compare button (disabled={!canCompare})
  • CredentialReviewSheet approve/reject/cancel buttons (disabled={busy}, disabled={!canConfirmReject})
  • DateTimePicker prev-month button (disabled={isPrevDisabled})
  • Calendar day Pressables (disabled={isPast})
  • Dashboard online toggle (missing accessibilityState for checked state — pattern P)

AT announces these as enabled — VoiceOver/TalkBack users discover the disabled state only by activating and getting no response.


M6. Error and Dynamic States Have No accessibilityLiveRegion

Section titled “M6. Error and Dynamic States Have No accessibilityLiveRegion”

Errors, status updates, and AI responses appear visually but AT is not notified:

  • Form validation errors (AddressAutocomplete, Input.tsx, all forms)
  • API error states (results, quotes/[id], professional/[id])
  • SOS countdown timer, status updates (should be "assertive")
  • ETA chip updates in tracking screen
  • Success overlays (booking confirmed, review submitted)
  • AI response streaming in chat/[bookingId].tsx

Exception (good): DispatchStatusBar.tsx correctly uses accessibilityLiveRegion="polite".


M7. Online Toggle Missing accessibilityState (WCAG 4.1.2)

Section titled “M7. Online Toggle Missing accessibilityState (WCAG 4.1.2)”

/app/(professional)/dashboard.tsx — professional online/offline toggle controls job dispatch eligibility. No accessibilityRole="switch", no accessibilityState={{ checked }}. A blind professional cannot verify their availability status.


M8. Primary List Items Missing All Accessibility

Section titled “M8. Primary List Items Missing All Accessibility”
  • BookingCard.tsx — no accessibilityRole, no accessibilityLabel
  • QueueRow in app/(admin)/credentials.tsx — no accessibilityRole, no accessibilityLabel
  • QuoteCard.tsx outer Pressable — no accessibilityRole, no accessibilityLabel

Exception (good): ProCard.tsx has proper accessibilityRole="button" and accessibilityLabel.


/components/chrome/TabBar.tsx — uses accessibilityRole="button" on tab items. Should be "tab". AT announces tabs as buttons, breaking tab panel navigation semantics.

Exception (good): SectionTabs.tsx uses "tab" correctly.


M10. ProgressBar — No Progress Semantics

Section titled “M10. ProgressBar — No Progress Semantics”

/components/triage/ProgressBar.tsx — Visual progress bar with numeric label {current}/{total} has no accessibilityRole="progressbar", no accessibilityValue={{ min: 0, max: total, now: current }}. AT cannot convey triage progress to user.


/components/MiniMap.tsx and /components/tracking/TrackingMap.tsx (implied) — Mapbox.MapView has no accessibilityLabel. AT reads nothing or raw SDK element names for the map area.


M12. Avatar Images — No accessibilityLabel

Section titled “M12. Avatar Images — No accessibilityLabel”

/components/Avatar.tsx (and inline avatar usages) — user/professional avatar images have no accessibilityLabel. AT announces “image” with no context about whose it is.


M13. PaginationDots — No Page Position Announced

Section titled “M13. PaginationDots — No Page Position Announced”

/components/PaginationDots.tsx — pure visual dots, no accessibilityLabel on container (e.g. “Step 2 of 3”). Used in welcome/onboarding flows. AT users don’t know their position in multi-step sequences.


  • Back buttons: padding: 4 → ~32pt total, fails 44pt iOS / 48dp Android
  • Header.tsx iconButton: 36×36pt — fails both platforms
  • MediaAttachments removeBtn: 22×22pt absolute positioned — fails both (hitSlop partial compensation only)
  • CompareSheet miniRemove: 16×16pt + hitSlop={8} → 32pt — still below iOS minimum

m1. Hardcoded Italian Strings (i18n + AT Language Mismatch)

Section titled “m1. Hardcoded Italian Strings (i18n + AT Language Mismatch)”
  • DateTimePicker.tsx “Orario” section label: hardcoded, not passed through i18n
  • DateTimePicker.tsx DAYS_IT / MONTHS_IT arrays: always Italian regardless of device locale — AT voice will mismatch if non-Italian locale set
  • Scattered throughout earlier-audited screens

m2. Chip/Toggle Pressables Missing accessibilityState={{ selected }}

Section titled “m2. Chip/Toggle Pressables Missing accessibilityState={{ selected }}”
  • AddCredentialSheet.tsx type selection chips
  • Review screen tag chips
  • TriageChips.tsx option chips
  • Pricing model toggles in onboarding

Exception (good): CategoryChips.tsx and QuestionChip.tsx correctly use accessibilityState={{ selected }}.


m3. HomeTopBar Avatar Pressable Missing Label

Section titled “m3. HomeTopBar Avatar Pressable Missing Label”

/components/home/HomeTopBar.tsx — avatar Pressable that navigates to profile has no accessibilityLabel.


Loading spinners in CredentialReviewSheet, PriceSummary, AddressAutocomplete, and several screens have no accessibilityLabel. AT announces “In Progress” (iOS default) without context of what is loading.


m5. ResultsSkeleton / SkeletonLoader AT Noise (see C8 above for critical tier)

Section titled “m5. ResultsSkeleton / SkeletonLoader AT Noise (see C8 above for critical tier)”

Beyond the critical shimmer animation, the skeleton list items produce meaningless navigation stops — minor compared to the animation issue but contributes to AT UX degradation during loading.


m6. FadeIn / FadeInDown on Pure Display Wrappers

Section titled “m6. FadeIn / FadeInDown on Pure Display Wrappers”

EmptyResults, PriceSummary, ProgressBar, PaginationDots, AddressAutocomplete dropdown — entrance animations on non-interactive containers. No user harm beyond vestibular risk (covered in C1), but unnecessary with useReducedMotion.


ScreenPath
Welcome / Continueapp/(welcome)/continue.tsx
Consumer Homeapp/(consumer)/index.tsx
Searchapp/(consumer)/search.tsx
Triageapp/(consumer)/triage.tsx
SOS Indexapp/sos/index.tsx
SOS Countdownapp/sos/[id]/countdown.tsx
SOS Trackingapp/sos/[id]/tracking.tsx
SOS Cancelledapp/sos/[id]/cancelled.tsx
SOS Completeapp/sos/[id]/complete.tsx
Resultsapp/results.tsx
Compareapp/compare.tsx
Professional Profileapp/professional/[id].tsx
Bookapp/book/[professionalId].tsx
Booking Detailapp/booking/[id].tsx
Chatapp/chat/[bookingId].tsx
Reviewapp/review/[bookingId].tsx
Settingsapp/settings.tsx
Consumer Bookingsapp/(consumer)/bookings.tsx
Consumer Profileapp/(consumer)/profile.tsx
Pro Dashboardapp/(professional)/dashboard.tsx
Pro Requestsapp/(professional)/requests.tsx
Pro Calendarapp/(professional)/calendar.tsx
Pro Credentialsapp/(professional)/credentials.tsx
Pro Onboarding (all steps)app/(professional)/onboarding/*
Verification Indexapp/verification/index.tsx
Verification Submitapp/verification/submit.tsx
Portfolioapp/portfolio/index.tsx
New Quoteapp/quotes/new.tsx
Quote Detailapp/quotes/[id].tsx
Stripe Onboardingapp/stripe-onboarding.tsx
Admin Credentialsapp/(admin)/credentials.tsx

AddCredentialSheet, CredentialCard, CategoryChips, PromptCard, MapAffordanceStrip, QuestionCard, QuestionChip, TriageChips, IdentityStrip, SectionTabs, StatsBar, StickyBookBar, ProCard, FilterChip, CompareSheet, EmptyResults, ResultsSkeleton, AddressAutocomplete, DateTimePicker, PriceSummary, CredentialReviewSheet, MediaAttachments, MiniMap, SkeletonLoader, QuoteCard, PaginationDots, ProgressBar (triage), Avatar (inferred), HomeTopBar (inferred), TabBar (inferred), Header (inferred), BookingCard (inferred), SOSButton (inferred), DispatchStatusBar (inferred), WelcomeTour (inferred — positive reference)


No screens were omitted from scope. All files under app/ and components/ were either directly read or had issues confirmed via cross-referencing systemic patterns against glob listings.


SeverityCountTop Instances
Critical8C1 reduced motion (every screen), C2 skeleton auto-animation, C3 DateTimePicker, C4 contrast, C5 SOS button, C6 stars, C7 onTouchEnd, C8 skeleton AT
Major14M1 icon hiding, M2 headers, M3 input labels, M4 button roles, M5 disabled state, M6 live regions, M7 online toggle, M8 list items, M9 TabBar role, M10 ProgressBar, M11 maps, M12 avatars, M13 pagination, M14 touch targets
Minor6m1 hardcoded Italian, m2 chip selected state, m3 HomeTopBar avatar, m4 ActivityIndicator, m5 skeleton noise, m6 display FadeIn

Highest-priority fixes (max impact, minimum effort):

  1. Gate all lib/animations.ts exports on useReducedMotion() — fixes C1 and C6 animation sub-issues in a single file
  2. Add accessibilityRole="button" + accessibilityLabel to Input.tsx TextInput — fixes M3 across ~30 forms
  3. Add accessibilityLabel + accessibilityRole="button" to SOSButton.tsx — fixes C5
  4. Add accessibilityElementsHidden={true} to all decorative icon wrappers — fixes M1
  5. Wrap SkeletonLoader shimmer in useReducedMotion() and add importantForAccessibility="no-hide-descendants" — fixes C2 and C8