Short answer
Flutter builds a Semantics tree that TalkBack and VoiceOver read; add clear labels to interactive elements and images, hide decorative elements, adhere to a touch target of at least 48×48 and sufficient color contrast, and test this automatically.
Explanation
Most Material widgets create semantics automatically, but custom elements (such as a GestureDetector around an icon) need Semantics with a label and a statement of their type (button). The tooltip in IconButton is read as a label. Use ExcludeSemantics for decorations and MergeSemantics to merge elements read as a unit. flutter_test checks the guidelines through meetsGuideline (target size, contrast, and element labels). Do not rely on color alone to convey information, and test manually by running a screen reader.
Code example
Semantics(
label: 'Add to favourites',
button: true,
child: GestureDetector(
onTap: addFavourite,
child: const Icon(Icons.favorite_border),
),
)
IconButton(
tooltip: 'Delete', // read by TalkBack / VoiceOver
onPressed: onDelete,
icon: const Icon(Icons.delete),
)
ExcludeSemantics(child: DecorativeBackground()) // purely visual, nothing to announce
testWidgets('meets accessibility guidelines', (tester) async {
final handle = tester.ensureSemantics();
await tester.pumpWidget(const MyApp());
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
await expectLater(tester, meetsGuideline(textContrastGuideline));
handle.dispose();
});Common mistakes
- Adding tappable icons with no semantic label, so the screen reader reads them just as "button" with no meaning.
- Relying on color alone to show errors or states, which low-vision or color-blind users cannot perceive.