Flutter builds a semantics tree next to the render tree and hands it to the platform's assistive technologies (TalkBack, VoiceOver, screen readers on web and desktop). Material widgets fill it in for you; you customize it with Semantics, MergeSemantics and ExcludeSemantics, respect the user's text scale through MediaQuery.textScalerOf, keep contrast and tap targets within the recommended minimums, and check all of it in widget tests with the Accessibility Guideline API.
- Every widget contributes to a semantics tree; built-in widgets (buttons, switches, text fields, Text) already set labels, roles and actions. Semantics(label:, button:, header:, hint:, value:) overrides or adds to that for custom widgets.
- MergeSemantics turns a subtree into one spoken node (a title plus a switch becomes one toggle); ExcludeSemantics and Image's excludeFromSemantics hide decorative content so it is not read twice.
- Test with real screen readers: TalkBack on Android, VoiceOver on iOS and macOS, NVDA or JAWS on Windows. On the web the semantics tree is built only after the user enables accessibility, unless you call SemanticsBinding.instance.ensureSemantics().
- Design targets from the official checklist: contrast of at least 4.5:1 for small text and 3:1 for large text, tap targets of at least 48x48 dp (Android) or 44x44 pt (iOS), and no context changes without user confirmation.
- Text scaling: read MediaQuery.textScalerOf(context).scale(fontSize) instead of the deprecated textScaleFactor; TextScaler models Android 14's non-linear scaling. Layouts must survive the largest OS font setting.
- Automate the checks: tester.ensureSemantics(), expectLater(tester, meetsGuideline(androidTapTargetGuideline | iOSTapTargetGuideline | labeledTapTargetGuideline | textContrastGuideline)) and tester.getSemantics(finder) with containsSemantics / matchesSemantics.
| Check | What to verify | Widgets / APIs | How to test | Target |
|---|---|---|---|---|
| Labels | Every interactive control announces a meaningful name and role | Tooltip on IconButton, Semantics(label:, button:) | TalkBack / VoiceOver, meetsGuideline(labeledTapTargetGuideline) | No unlabeled tap targets |
| Tap target size | Interactive elements are big enough to hit | SizedBox / ConstrainedBox around custom tappables; IconButton defaults to 48x48 | meetsGuideline(androidTapTargetGuideline), meetsGuideline(iOSTapTargetGuideline) | 48x48 dp Android, 44x44 pt iOS |
| Text contrast | Text is readable against its background | ThemeData colors, ColorScheme | meetsGuideline(textContrastGuideline), Accessibility Scanner (Android), Accessibility Inspector (Xcode) | 4.5:1 small text, 3:1 large text |
| Text scaling | Layout survives the largest OS font size | MediaQuery.textScalerOf(context).scale(size), flexible layouts, MediaQuery.withClampedTextScaling only where unavoidable | Device with largest font setting; pumpWidget inside MediaQuery(data: MediaQueryData(textScaler: TextScaler.linear(2.0))) | No clipping or overflow at 2x |
| Grouping | Related pieces are read as one item, in the right order | MergeSemantics, ListTile (merges internally) | tester.getSemantics(finder) and containsSemantics(...) | One node per logical control |
| Decorative content | Decorative images and duplicated text are silent | ExcludeSemantics, Image(excludeFromSemantics: true) | Screen reader walk-through | Nothing redundant announced |
| Structure | Headings and sections are discoverable | Semantics(header: true), Semantics(container: true) | VoiceOver rotor / TalkBack headings navigation | Headings announced as headings |
| Color-independent cues | State is not conveyed by color alone | Icons, text, underline in addition to color | Grayscale / color-blind simulation | Usable in grayscale |
The left column mirrors Flutter's release checklist; the right columns map each item to the widgets and test helpers that address it.
The semantics tree is the audio-description script of your UI
A screen reader does not look at pixels; it reads a script that Flutter writes alongside the picture. Each line of the script says what an element is (a button, a heading, a switch that is on), what it is called, and what can be done with it. Material widgets write their own lines automatically. Semantics lets you rewrite a line, MergeSemantics joins several lines into one sentence, and ExcludeSemantics deletes lines that would only add noise.
Minimal working example
import 'package:flutter/material.dart';
class LikeButton extends StatelessWidget {
const LikeButton({super.key, required this.liked, required this.onToggle});
final bool liked;
final VoidCallback onToggle;
@override
Widget build(BuildContext context) {
return Semantics(
button: true,
label: liked ? 'Unlike' : 'Like',
toggled: liked,
child: GestureDetector(
onTap: onToggle,
child: ExcludeSemantics(
child: SizedBox(
width: 48,
height: 48,
child: Icon(
liked ? Icons.favorite : Icons.favorite_border,
color: liked ? Colors.red : null,
),
),
),
),
);
}
}- L11Semantics adds or overrides properties on the node that this subtree produces; GestureDetector already contributes the tap action.
- L12button: true gives the node the button role, so TalkBack says 'Like, button' instead of just 'Like'.
- L13The label describes the action in its current state; the icon itself has no text to read.
- L14toggled exposes the on/off state so the screen reader can announce it and so tests can assert it.
- L17ExcludeSemantics hides the inner icon; without it an Icon with a semanticLabel would be read a second time.
- L19A 48x48 box satisfies the Android tap-target guideline even though the glyph is 24 px.
Old vs new
Android 14 scales large text less aggressively than small text, so a single multiplier can no longer describe the user's preference. TextScaler.scale(fontSize) applies whatever curve the platform uses; textScaleFactor is kept only as a deprecated estimate. textScalerOf also avoids rebuilding on every unrelated MediaQuery change.
| 1 | − | final factor = MediaQuery.of(context).textScaleFactor; |
| 2 | − | final iconSize = 24 * factor; |
| 1 | + | final scaler = MediaQuery.textScalerOf(context); |
| 2 | + | final iconSize = scaler.scale(24); |
Raw before / after
final factor = MediaQuery.of(context).textScaleFactor; final iconSize = 24 * factor;
final scaler = MediaQuery.textScalerOf(context); final iconSize = scaler.scale(24);
Use cases
- You build a custom control from GestureDetector, InkWell or a RenderObject and it needs a role, label and state for screen readers.
- A card or list row contains several texts and one action and should be read as one item, in a sensible order.
- Your layout uses fixed heights or single-line text and must be checked (and fixed) for the largest font setting.
- You ship to markets or customers that require WCAG 2 / EN 301 549 conformance and need automated evidence in CI.
Benefits
- Built-in widgets already carry correct semantics, so most apps start close to accessible and only custom widgets need work.
- The Accessibility Guideline API turns contrast, tap-target and label checks into ordinary widget tests that fail in CI before a user hits the problem.
- Accessible layouts (flexible sizing, sufficient contrast, bigger targets) also help sighted users in sunlight, on small screens or with one hand.
Limitations
- The guideline matchers cover contrast, target size and labels only; reading order, focus management and meaningful descriptions still need manual screen-reader testing.
- Web apps do not build the semantics tree until accessibility is enabled (by the user or ensureSemantics), and the web output is a separate DOM overlay that can differ from mobile behaviour.
- Semantics only describes what the widget tree exposes; a custom painter or platform view needs explicit Semantics or native accessibility work.
Alternatives and trade-offs
- Material and Cupertino widgets — Prefer IconButton, Switch, ListTile and friends over hand-rolled GestureDetector trees; they already provide roles, labels, states and 48 px targets.
- Adaptive layout techniques — Large-font problems are usually layout problems: Expanded, Wrap, SingleChildScrollView and LayoutBuilder solve them without touching semantics.
- Widget tests — Guideline tests are regular widget tests; see the testing topic for finders, pumping and golden tests.
- Platform accessibility scanners — Android Accessibility Scanner and Xcode's Accessibility Inspector audit the running app, including native views and colours Flutter cannot see in a unit test.
Icon-only controls without a label
Problem: A GestureDetector or InkWell wrapped around an Icon produces a node with a tap action but no name; TalkBack announces just 'button' or nothing at all, and labeledTapTargetGuideline fails.
Fix: Use IconButton with a tooltip (it doubles as the semantic label), or wrap the custom control in Semantics(label: ..., button: true).
GestureDetector(
onTap: close,
child: const Icon(Icons.close),
)IconButton(
tooltip: 'Close',
onPressed: close,
icon: const Icon(Icons.close),
)Tap targets smaller than 48x48
Problem: A 24 px icon or a short text link is hard to hit for users with limited dexterity, and androidTapTargetGuideline reports it.
Fix: Give custom tappables a minimum box: SizedBox(width: 48, height: 48) or ConstrainedBox(constraints: const BoxConstraints(minWidth: 48, minHeight: 48)). The visual glyph can stay small; only the hit area must grow.
Layouts that break at large font sizes
Problem: Fixed-height rows, maxLines: 1 without overflow handling and Row children without Expanded produce clipped text or RenderFlex overflows when the user picks the largest font setting.
Fix: Size by content (Expanded, Wrap, SingleChildScrollView), switch Row to Column when MediaQuery.textScalerOf(context).scale(baseSize) is large, and test on a device with the biggest font. Clamp scaling with MediaQuery.withClampedTextScaling only for elements that genuinely cannot grow, such as a tab bar label.
No automated accessibility check in the test suite
Problem: Contrast and tap-target regressions are invisible in code review and only surface when a user complains or a store audit fails.
Fix: Add a widget test per screen that enables semantics and asserts the Flutter guidelines, plus targeted semantics assertions for custom controls.
testWidgets('renders', (tester) async {
await tester.pumpWidget(const MyApp());
expect(find.byType(Switch), findsOneWidget); // says nothing about a11y
});import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/main.dart';
void main() {
testWidgets('settings screen meets accessibility guidelines', (tester) async {
final handle = tester.ensureSemantics();
await tester.pumpWidget(const MyApp());
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
await expectLater(tester, meetsGuideline(iOSTapTargetGuideline));
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
await expectLater(tester, meetsGuideline(textContrastGuideline));
final node = tester.getSemantics(find.byType(Switch));
expect(node, containsSemantics(hasToggledState: true, hasTapAction: true));
handle.dispose();
});
}This page is Boring Doc’s independent explanation. For the complete specification use the official pages below (content © The Flutter Authors, CC BY 3.0; code samples BSD). Dates show when the explanation was last checked and when the official page last changed.
- Accessibility overview and release checklist Flutter 3.47.7 Explanation verified against current sourcehttps://docs.flutter.dev/ui/accessibility
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- Accessibility technologies (screen readers, mobility support) Flutter 3.47.7 Explanation verified against current sourcehttps://docs.flutter.dev/ui/accessibility/assistive-technologies
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- UI design and styling (large fonts, contrast, tap targets) Flutter 3.47.7 Explanation verified against current sourcehttps://docs.flutter.dev/ui/accessibility/ui-design-and-styling
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/ui/accessibility/accessibility-testing
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- Semantics class API reference Flutter 3.47.7https://api.flutter.dev/flutter/widgets/Semantics-class.html
- Last checked
- Oct 21, 2026
AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.