high priority low complexity frontend pending frontend specialist Tier 5

Acceptance Criteria

Badge renders on the Notifications tab only when count > 0; hidden when count is 0
Badge count updates reactively when the Riverpod notification count provider value changes
Semantics label for the Notifications tab reads 'Notifications, {n} unread notifications, Tab 5 of 5' when count > 0
Semantics label reads 'Notifications, Tab 5 of 5' when count is 0 (no badge noise)
Badge background-to-text contrast ratio is ≥ 4.5:1 verified against the design token color system
Badge displays a capped label (e.g., '99+') when count exceeds 99 to prevent overflow
Badge does not overlap or obscure the tab icon or label
Touch target of the Notifications tab remains ≥ 44×44dp with the badge present
Badge passes AccessibilityAuditRunner with zero WCAG 2.2 AA violations
Widget test covers: badge hidden at count 0, badge shows correct value at count 3, badge shows '99+' at count 150, Semantics label is correct at each state

Technical Requirements

frameworks
Flutter
Riverpod
data models
NotificationCount (int, provided via Riverpod notifier)
performance requirements
Badge re-renders only when notification count changes — use select() or equivalent to prevent unnecessary rebuilds
No layout jank during badge count transitions
security requirements
Badge count must not expose sensitive notification content in the Semantics tree beyond the numeric count
ui components
AccessibleBottomNavBar (existing, to be modified)
NotificationBadge (new overlay widget using Stack + Positioned)
Design token colors for badge background and text (e.g., token: color-error or color-notification-badge)

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Stack a small circular badge widget over the Notifications tab icon using Stack + Positioned. Source the badge from a ConsumerWidget or Consumer widget watching a notificationCountProvider (Riverpod). Build the full Semantics label string inside the tab's Semantics widget rather than relying on screen reader concatenation — screen readers for VoiceOver (iOS) and TalkBack (Android) handle concatenation differently, so explicit label construction is safer. Use design tokens for all colors; do not hardcode hex values.

Cap at 99+ to avoid layout overflow. Keep badge hidden (opacity 0 or conditional widget removal) at count 0 so the Semantics label stays clean.

Testing Requirements

Write flutter_test widget tests covering: (1) badge hidden when count provider emits 0, (2) badge shows '3' when provider emits 3, (3) badge shows '99+' when provider emits 150, (4) Semantics label is correct for each state using tester.getSemantics(), (5) touch target remains ≥ 44×44dp with badge present, (6) AccessibilityAuditRunner passes with no violations. Use ProviderScope with override to inject controlled count values.

Component
Accessible Bottom Navigation Bar
ui medium
Epic Risks (3)
high impact high prob technical

Flutter's ModalBottomSheet and showDialog do not automatically confine VoiceOver or TalkBack focus to the modal's subtree on all platform versions. Background content may remain reachable by screen readers, confusing users and violating WCAG 2.2 criterion 1.3.1.

Mitigation & Contingency

Mitigation: Wrap modal content in an ExcludeSemantics or BlockSemantics widget for background content. Use a Semantics node with liveRegion on the modal container and manually request focus via FocusScope after the modal animation completes. Test on both iOS (VoiceOver) and Android (TalkBack) during widget development.

Contingency: If platform-level focus trapping is unreliable, implement a custom modal wrapper widget that uses a FocusTrap widget (available in Flutter's internal tooling) and an Overlay entry with semantics blocking on the dimmed background layer.

medium impact medium prob technical

On iOS, the system-level swipe-back gesture (UINavigationController) can bypass PopScope and GoRouter's gesture suppression, meaning users can still accidentally dismiss screens via swipe even after the component is implemented. This breaks the gesture-free contract for motor-impaired users.

Mitigation & Contingency

Mitigation: Set popGestureEnabled: false in GoRouter route configurations where swipe-back is suppressed. Test specifically against Flutter's CupertinoPageRoute, which respects this flag, and verify that GoRouter generates Cupertino routes on iOS rather than Material routes with gesture enabled.

Contingency: If go_router's popGestureEnabled flag does not propagate correctly, wrap affected routes in a WillPopScope replacement (PopScope with canPop: false) and file a bug with the go_router maintainers. Document the workaround in the navigation-route-config component for future maintainers.

medium impact medium prob scope

The feature description implies migrating all existing ModalBottomSheet and dialog call sites across the app to use the new accessible helpers, which is a cross-cutting change. Scope underestimation could mean the epic finishes the new components but leaves many call sites un-migrated, leaving the accessibility promise partially broken.

Mitigation & Contingency

Mitigation: Audit all existing modal call sites at the start of the epic (grep for showModalBottomSheet, showDialog, showCupertinoDialog) and add the count to the task list. Treat migration as explicit tasks, not an implied post-step.

Contingency: If migration scope grows beyond the epic's estimate, create a follow-up tech-debt epic scoped only to call-site migration, and gate the release on at minimum all flows used by the accessibility user-story acceptance criteria being migrated.