high priority low complexity frontend pending frontend specialist Tier 4

Acceptance Criteria

Active tab icon switches from outlined to filled variant (e.g., Icons.home_outlined → Icons.home) on selection
Active tab label has a visible underline or pill indicator that distinguishes it from inactive labels
Active tab icon and label use the app's primary/accent color from design tokens; inactive tabs use the muted/secondary icon color
The contrast ratio between the active indicator color and the nav bar background meets WCAG 2.2 non-text contrast of at least 3:1
The contrast ratio between inactive icon color and nav bar background also meets 3:1
Semantics(selected: true) is set on the active NavigationDestination; Semantics(selected: false) on all others
VoiceOver announces 'selected' when focus lands on the active tab and does not announce 'selected' for inactive tabs
TalkBack announces 'selected' for the active tab in the same manner
Active state transitions animate within 200 ms using the app's standard animation curve
Active indicators are visible in both light mode and dark mode (if the app supports theme switching)

Technical Requirements

frameworks
Flutter
apis
NavigationBar.indicatorColor
NavigationBarTheme
Semantics(selected:)
AnimatedSwitcher (for icon swap animation)
ThemeData (for NavigationBarTheme override)
performance requirements
Icon swap animation must complete within 200 ms and must not jank; use implicit animation widgets
Active state color resolution must use design tokens at build time, not computed at paint time
ui components
AccessibleBottomNavBar (enhanced from task-012)
NavigationBarTheme
AnimatedSwitcher or AnimatedIcon
Design token color references

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Define the active and inactive icon pairs as a static list of TabIconPair(outlined, filled) keyed to each tab index. In the NavigationDestination builder, select filled vs outlined based on whether the tab index matches TabStateManager's current index. Apply NavigationBarTheme at the MaterialApp or Scaffold level to set indicatorColor to AppColors.navIndicator (a design token), labelTextStyle for active state, and iconTheme for active/inactive states — this keeps color logic out of the widget body. For the underline/pill: NavigationBar's built-in indicator pill (indicatorColor) is sufficient; do not add a second underline unless the design spec explicitly requires it, to avoid visual clutter.

Compute and document contrast ratios for the chosen token colors in the PR description using the WCAG 2.2 relative luminance formula. If dark mode is in scope, ensure AppColors.navIndicator has a dark-mode variant in the token system.

Testing Requirements

Write widget tests verifying: (1) active tab renders filled icon and inactive tabs render outlined icon, (2) Semantics selected flag is true only for active tab, (3) color of active icon matches the design token value. Run flutter_test accessibilityGuidelines check to assert contrast ratios. Use the Color contrast checker utility (or a helper that computes relative luminance) to assert 3:1 ratio between active indicator color and nav bar background in test. Capture golden screenshots for all five active states.

Manually test with TalkBack and VoiceOver to confirm 'selected' announcement.

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.