Ideony Mobile — Accessibility Audit
Ideony Mobile — Accessibility Audit
Section titled “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
CRITICAL — WCAG AA Violations
Section titled “CRITICAL — WCAG AA Violations”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.tsx — withRepeat(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, noaccessibilityLabel, noaccessibilityState={{ disabled: isPrevDisabled }} - Each calendar day Pressable: no
accessibilityRole,accessibilityLabel= only the bare digit (e.g. “4”), noaccessibilityState={{ selected, disabled }} - SlotChip Pressable: no
accessibilityRole, noaccessibilityState={{ 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:
| Location | Text / Background | Size | Risk |
|---|---|---|---|
IdentityStrip.tsx verifiedLabel | textSecondary on surface | 11pt | Fails |
TrustTierBadge ELITE | light text on dark olive | 11pt | Fails |
VerifiedBadge label | textSecondary on primarySubtle | 11pt | Fails |
CredentialCard status badge | colored text on tinted bg | 11pt | Fails |
| Admin credential type badge | colored text on tinted bg | 11pt | Fails |
CompareSheet miniName | text on surface | 11pt | Verify |
PriceSummary fine-print note | textTertiary on background | 11pt | Fails |
ProgressBar label | text at opacity: 0.5 | 12pt | Fails |
TriageChips option text | textSecondary on surface | 13pt | Verify |
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) — haveaccessibilityLabelbut norole - Back buttons on 20+ screens:
padding: 4→ 32pt, no role, no label Header.tsxiconButton 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 }}:
CompareSheetcompare button (disabled={!canCompare})CredentialReviewSheetapprove/reject/cancel buttons (disabled={busy},disabled={!canConfirmReject})DateTimePickerprev-month button (disabled={isPrevDisabled})- Calendar day Pressables (
disabled={isPast}) - Dashboard online toggle (missing
accessibilityStatefor 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— noaccessibilityRole, noaccessibilityLabelQueueRowinapp/(admin)/credentials.tsx— noaccessibilityRole, noaccessibilityLabelQuoteCard.tsxouter Pressable — noaccessibilityRole, noaccessibilityLabel
Exception (good): ProCard.tsx has proper accessibilityRole="button" and accessibilityLabel.
M9. TabBar Wrong accessibilityRole
Section titled “M9. TabBar Wrong accessibilityRole”/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.
M11. Map Widgets — No Description
Section titled “M11. Map Widgets — No Description”/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.
M14. Touch Target Failures
Section titled “M14. Touch Target Failures”- Back buttons:
padding: 4→ ~32pt total, fails 44pt iOS / 48dp Android Header.tsxiconButton: 36×36pt — fails both platformsMediaAttachmentsremoveBtn: 22×22pt absolute positioned — fails both (hitSlop partial compensation only)CompareSheetminiRemove: 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 i18nDateTimePicker.tsxDAYS_IT/MONTHS_ITarrays: 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.tsxtype selection chipsReviewscreen tag chipsTriageChips.tsxoption 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.
m4. ActivityIndicator Missing Labels
Section titled “m4. ActivityIndicator Missing Labels”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.
Screens Audited
Section titled “Screens Audited”| Screen | Path |
|---|---|
| Welcome / Continue | app/(welcome)/continue.tsx |
| Consumer Home | app/(consumer)/index.tsx |
| Search | app/(consumer)/search.tsx |
| Triage | app/(consumer)/triage.tsx |
| SOS Index | app/sos/index.tsx |
| SOS Countdown | app/sos/[id]/countdown.tsx |
| SOS Tracking | app/sos/[id]/tracking.tsx |
| SOS Cancelled | app/sos/[id]/cancelled.tsx |
| SOS Complete | app/sos/[id]/complete.tsx |
| Results | app/results.tsx |
| Compare | app/compare.tsx |
| Professional Profile | app/professional/[id].tsx |
| Book | app/book/[professionalId].tsx |
| Booking Detail | app/booking/[id].tsx |
| Chat | app/chat/[bookingId].tsx |
| Review | app/review/[bookingId].tsx |
| Settings | app/settings.tsx |
| Consumer Bookings | app/(consumer)/bookings.tsx |
| Consumer Profile | app/(consumer)/profile.tsx |
| Pro Dashboard | app/(professional)/dashboard.tsx |
| Pro Requests | app/(professional)/requests.tsx |
| Pro Calendar | app/(professional)/calendar.tsx |
| Pro Credentials | app/(professional)/credentials.tsx |
| Pro Onboarding (all steps) | app/(professional)/onboarding/* |
| Verification Index | app/verification/index.tsx |
| Verification Submit | app/verification/submit.tsx |
| Portfolio | app/portfolio/index.tsx |
| New Quote | app/quotes/new.tsx |
| Quote Detail | app/quotes/[id].tsx |
| Stripe Onboarding | app/stripe-onboarding.tsx |
| Admin Credentials | app/(admin)/credentials.tsx |
Components Audited
Section titled “Components Audited”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)
Screens NOT Audited
Section titled “Screens NOT Audited”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.
Summary Table
Section titled “Summary Table”| Severity | Count | Top Instances |
|---|---|---|
| Critical | 8 | C1 reduced motion (every screen), C2 skeleton auto-animation, C3 DateTimePicker, C4 contrast, C5 SOS button, C6 stars, C7 onTouchEnd, C8 skeleton AT |
| Major | 14 | M1 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 |
| Minor | 6 | m1 hardcoded Italian, m2 chip selected state, m3 HomeTopBar avatar, m4 ActivityIndicator, m5 skeleton noise, m6 display FadeIn |
Highest-priority fixes (max impact, minimum effort):
- Gate all
lib/animations.tsexports onuseReducedMotion()— fixes C1 and C6 animation sub-issues in a single file - Add
accessibilityRole="button"+accessibilityLabeltoInput.tsxTextInput — fixes M3 across ~30 forms - Add
accessibilityLabel+accessibilityRole="button"toSOSButton.tsx— fixes C5 - Add
accessibilityElementsHidden={true}to all decorative icon wrappers — fixes M1 - Wrap
SkeletonLoadershimmer inuseReducedMotion()and addimportantForAccessibility="no-hide-descendants"— fixes C2 and C8