Skip to content
Boring Doc
Boring Doc explanation Verified against Flutter 3.47.7 3 min readaccessibilitya11ySemanticsscreen readerTalkBack

Accessibility

Semantics, screen readers, contrast and focus.

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.

  1. 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.
  2. 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.
  3. 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().
  4. 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.
  5. 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.
  6. Automate the checks: tester.ensureSemantics(), expectLater(tester, meetsGuideline(androidTapTargetGuideline | iOSTapTargetGuideline | labeledTapTargetGuideline | textContrastGuideline)) and tester.getSemantics(finder) with containsSemantics / matchesSemantics.
ComparisonAccessibility checks: what, how to verify, and the target
CheckWhat to verifyWidgets / APIsHow to testTarget
LabelsEvery interactive control announces a meaningful name and roleTooltip on IconButton, Semantics(label:, button:)TalkBack / VoiceOver, meetsGuideline(labeledTapTargetGuideline)No unlabeled tap targets
Tap target sizeInteractive elements are big enough to hitSizedBox / ConstrainedBox around custom tappables; IconButton defaults to 48x48meetsGuideline(androidTapTargetGuideline), meetsGuideline(iOSTapTargetGuideline)48x48 dp Android, 44x44 pt iOS
Text contrastText is readable against its backgroundThemeData colors, ColorSchememeetsGuideline(textContrastGuideline), Accessibility Scanner (Android), Accessibility Inspector (Xcode)4.5:1 small text, 3:1 large text
Text scalingLayout survives the largest OS font sizeMediaQuery.textScalerOf(context).scale(size), flexible layouts, MediaQuery.withClampedTextScaling only where unavoidableDevice with largest font setting; pumpWidget inside MediaQuery(data: MediaQueryData(textScaler: TextScaler.linear(2.0)))No clipping or overflow at 2x
GroupingRelated pieces are read as one item, in the right orderMergeSemantics, ListTile (merges internally)tester.getSemantics(finder) and containsSemantics(...)One node per logical control
Decorative contentDecorative images and duplicated text are silentExcludeSemantics, Image(excludeFromSemantics: true)Screen reader walk-throughNothing redundant announced
StructureHeadings and sections are discoverableSemantics(header: true), Semantics(container: true)VoiceOver rotor / TalkBack headings navigationHeadings announced as headings
Color-independent cuesState is not conveyed by color aloneIcons, text, underline in addition to colorGrayscale / color-blind simulationUsable 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.

Where the analogy breaks: A script is written once; the semantics tree is regenerated every time the UI changes, and it also has to carry actions (tap, scroll, increase), not just descriptions. The analogy also hides the non-visual half of accessibility: text scaling, contrast and tap-target size matter for sighted users with low vision or limited dexterity, and no amount of scripting fixes a 20-pixel button.

Minimal working example

A custom tappable icon with proper semantics
dart
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,
            ),
          ),
        ),
      ),
    );
  }
}
Expected: VoiceOver announces 'Like, button' (or 'Unlike, button, selected') and a double tap triggers onToggle. meetsGuideline(androidTapTargetGuideline) and labeledTapTargetGuideline both pass.Flutter ≥ 3.0.0Semantics API docs
  1. L11Semantics adds or overrides properties on the node that this subtree produces; GestureDetector already contributes the tap action.
  2. L12button: true gives the node the button role, so TalkBack says 'Like, button' instead of just 'Like'.
  3. L13The label describes the action in its current state; the icon itself has no text to read.
  4. L14toggled exposes the on/off state so the screen reader can announce it and so tests can assert it.
  5. L17ExcludeSemantics hides the inner icon; without it an Icon with a semanticLabel would be read a second time.
  6. 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.

dart+2−2
Linear scaling with textScaleFactor (deprecated since 3.16)
1−final factor = MediaQuery.of(context).textScaleFactor;
2−final iconSize = 24 * factor;
TextScaler
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).

✗ Nameless tap target
dart
GestureDetector(
  onTap: close,
  child: const Icon(Icons.close),
)
✓ Labelled and 48x48 by default
dart
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.

✗ Semantics never enabled, so guideline tests see an empty tree
dart
testWidgets('renders', (tester) async {
  await tester.pumpWidget(const MyApp());
  expect(find.byType(Switch), findsOneWidget); // says nothing about a11y
});
✓ Guideline API in a widget test
dart
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 source
    https://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 source
    https://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 source
    https://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
  • Accessibility testing Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/ui/accessibility/accessibility-testing
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • https://api.flutter.dev/flutter/widgets/Semantics-class.html
    Last checked
    Oct 21, 2026
Was this useful?
AI explanationAI-generated · grounded in cited sources
AI explanations are not configured
Set 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.