Accessibility (a11y)

Medium PriorityAsked in ~45% of senior interviews

4 min read

UX & Compliance

Most Material widgets are accessible by default — the real work is not breaking that with custom widgets, and explicitly labelling icons, images, and custom interactions.

DimensionWhat to verifyTools
Screen readerTalkBack (Android) / VoiceOver (iOS) announce meaningfullySemantics widget, SemanticsDebugger
ContrastText vs background ≥ 4.5:1 (AA), 7:1 (AAA)WebAIM contrast checker, design review
Touch targetsTappable area ≥ 48×48 dpkMinInteractiveDimension (= 48)
Text scalingLayout doesn't break at 200% font sizeDevice settings → text size, MediaQuery.textScalerOf
Reduced motionAnimations shorten/disable when OS preference is setMediaQuery.disableAnimationsOf(context)
Focus / keyboardTab + arrow keys navigate; focused element visibleFocusableActionDetector, Shortcuts, Actions

Code in action

// Semantic label on an icon button (icons alone aren't meaningful to screen readers)
IconButton(
  icon: const Icon(Icons.delete),
  onPressed: deleteItem,
  tooltip: 'Delete',                          // also acts as semantic label
);
 
// Or explicitly via Semantics
Semantics(
  label: 'Delete item',
  hint: 'Double tap to delete',
  button: true,
  child: IconButton(icon: const Icon(Icons.delete), onPressed: deleteItem),
);
 
// Describe images
Semantics(
  image: true,
  label: 'Profile photo of Alice',
  child: CircleAvatar(backgroundImage: NetworkImage(url)),
);
 
// Skip purely decorative widgets
Semantics(
  excludeSemantics: true,
  child: Image.asset('background_pattern.png'),
);
 
// Group related semantics
MergeSemantics(
  child: Row(children: [
    const Icon(Icons.star),
    const SizedBox(width: 4),
    const Text('Featured'),
  ]),
);
// Screen reader: "Featured" (announced once, not "Star, Featured")
// Respect "reduce motion"
final reduced = MediaQuery.disableAnimationsOf(context);
 
return AnimatedContainer(
  duration: reduced ? Duration.zero : const Duration(milliseconds: 300),
  width: _expanded ? 200 : 100,
);
 
// Text scales by default — don't fight it
Text('Hello', style: Theme.of(context).textTheme.bodyLarge);
// ❌ textScaler: TextScaler.linear(1.0)  // breaks accessibility
 
// Touch targets — ensure 48×48dp minimum
SizedBox(
  width: kMinInteractiveDimension,
  height: kMinInteractiveDimension,
  child: IconButton(icon: const Icon(Icons.close), onPressed: () {}),
);

Focus and keyboard navigation

// Make a custom widget focusable + keyboard-actionable
FocusableActionDetector(
  shortcuts: const {
    SingleActivator(LogicalKeyboardKey.enter):    ActivateIntent(),
    SingleActivator(LogicalKeyboardKey.space):    ActivateIntent(),
  },
  actions: {
    ActivateIntent: CallbackAction(onInvoke: (_) => onTap()),
  },
  child: const Material(
    child: InkWell(child: Padding(padding: EdgeInsets.all(16),
      child: Text('Custom button'))),
  ),
);

Material widgets (ElevatedButton, IconButton, etc.) handle this for free. Custom interactive widgets need to opt in.


Testing accessibility

testWidgets('delete button has correct semantics', (tester) async {
  await tester.pumpWidget(MyWidget());
 
  final s = tester.getSemantics(find.byType(IconButton));
  expect(s.label, 'Delete item');
  expect(s.hasAction(SemanticsAction.tap), true);
});
 
testWidgets('respects reduced motion', (tester) async {
  tester.platformDispatcher.accessibilityFeaturesTestValue =
      const FakeAccessibilityFeatures(disableAnimations: true);
  // assert animation duration is zero
});
ToolUse
SemanticsDebugger widgetWrap your app to visually see semantics tree
flutter test + tester.getSemantics(...)Unit-level a11y assertions
Android Accessibility ScannerCatches low contrast, small targets, unlabelled controls
Xcode Accessibility InspectoriOS equivalent, with audit + live navigation
accessibility_audit CI stepflutter analyze with custom lint rules

Common mistakes to avoid

❌ IconButton with no label / tooltip
   Screen reader announces "Button. Double tap to activate." — useless.
   ✅ Always provide tooltip or Semantics label.

❌ Clamping textScaleFactor to 1.0
   "It breaks our layout at 200%" → fix the layout, don't break the feature.
   ✅ Make layouts elastic; test at 1.5x and 2x.

❌ Color as the only state indicator
   "Required fields are red" — colorblind users can't tell.
   ✅ Pair color with icon or text.

❌ Tap targets smaller than 48×48dp
   Easy to mis-tap; accessibility scanners fail.
   ✅ kMinInteractiveDimension or wrap small icons in Padding.

❌ Animations that don't respect reduce-motion
   Some users (vestibular disorders) get sick from parallax / heavy motion.
   ✅ Check MediaQuery.disableAnimationsOf(context).

❌ Custom gestures without keyboard equivalents
   Swipe-to-delete with no alternative → keyboard / switch users stuck.
   ✅ Provide an action menu or keyboard shortcut.

❌ "We'll fix accessibility later"
   Retrofitting is 10× harder. Build a11y-aware from day one.

❌ Forgetting the announce action for dynamic content
   Toast appears; screen reader user has no idea.
   ✅ SemanticsService.announce('Item deleted', TextDirection.ltr);

Interview follow-ups

  1. How do you handle accessibility for a custom-painted widget (CustomPaint)? Wrap or compose with Semantics(...) to describe what's drawn, since screen readers can't read paint commands. For a custom chart, semantically describe the data ("Bar chart with 5 categories, January value 42, February value 38..."). For interactive painted regions, use Semantics with onTap + nested labels per region.

  2. What's the difference between excludeSemantics, MergeSemantics, and BlockSemantics? excludeSemantics: true — drop this subtree from the accessibility tree (purely decorative). MergeSemantics — combine children's semantics into one node (e.g., icon + text read as one). BlockSemantics — sibling content underneath is hidden from screen readers (used for modal overlays).

  3. How do you avoid the "clamp textScaleFactor" anti-pattern? Design with elasticity: don't hard-code heights for text containers, use Flexible/Expanded, let text wrap or scroll. Test at 1.5x and 2x font size — they're real user settings. If a screen breaks, fix the layout (more vertical space, scrollable content), don't disable scaling.

How helpful was this content?

Please sign in to rate this article.