Accessibility (a11y)
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.
| Dimension | What to verify | Tools |
|---|---|---|
| Screen reader | TalkBack (Android) / VoiceOver (iOS) announce meaningfully | Semantics widget, SemanticsDebugger |
| Contrast | Text vs background ≥ 4.5:1 (AA), 7:1 (AAA) | WebAIM contrast checker, design review |
| Touch targets | Tappable area ≥ 48×48 dp | kMinInteractiveDimension (= 48) |
| Text scaling | Layout doesn't break at 200% font size | Device settings → text size, MediaQuery.textScalerOf |
| Reduced motion | Animations shorten/disable when OS preference is set | MediaQuery.disableAnimationsOf(context) |
| Focus / keyboard | Tab + arrow keys navigate; focused element visible | FocusableActionDetector, 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
});| Tool | Use |
|---|---|
SemanticsDebugger widget | Wrap your app to visually see semantics tree |
flutter test + tester.getSemantics(...) | Unit-level a11y assertions |
| Android Accessibility Scanner | Catches low contrast, small targets, unlabelled controls |
| Xcode Accessibility Inspector | iOS equivalent, with audit + live navigation |
accessibility_audit CI step | flutter 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
-
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, useSemanticswithonTap+ nested labels per region. -
What's the difference between
excludeSemantics,MergeSemantics, andBlockSemantics?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). -
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.