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. Why are Material widgets "accessible by default" and what does that mean exactly? They ship with appropriate Semantics baked in — ElevatedButton reports as a button, Switch as a toggle, etc. They also respect text scaling, focus, and contrast through the theme. You break that only if you wrap them in opaque custom widgets that don't forward semantics. Most a11y bugs come from custom widgets, not Material ones.

  2. 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.

  3. 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).

  4. 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.