skip to content

Accessibility & Locales

How a Flutter app adapts to its users: a semantics tree for screen readers, text scaling and tap targets, ARB-generated translations, and right-to-left layout with locale formats.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

21

In Flutter, how do you give an icon-only button and a decorative image the right screen-reader announcement?

level: juniorimportance: must knowfreq 58%

answer

  1. the semantics tree, not the pixels
  2. IconButton tooltip becomes the label
  3. semanticLabel on Icon and Image
  4. excludeFromSemantics for decoration
  5. Text spells it semanticsLabel

basics

~10 s

Give the icon button a tooltip, or its Icon a semanticLabel, so TalkBack and VoiceOver read a name plus "button". Hide a purely decorative image with Image's excludeFromSemantics: true, or wrap it in ExcludeSemantics.

solid answer

~40 s

Screen readers read Flutter's **semantics tree**, not its pixels, so an `IconButton` with only an icon is announced as an unnamed button. Pass `tooltip: 'Play'` (it supplies the label and a long-press tooltip) or give the `Icon` a `semanticLabel`. An informative `Image` needs `semanticLabel: 'Episode artwork for ...'`; a decorative one needs `excludeFromSemantics: true`, or an `ExcludeSemantics` wrapper, so it is skipped instead of read as an unlabeled image. For a custom tappable widget built from `GestureDetector`, wrap it in `Semantics(label: 'Play', button: true, onTap: ...)` so it has a name, a role and an action. Mind the spelling: `Icon` and `Image` take `semanticLabel`, while `Text` takes `semanticsLabel` to replace what is read for its visible text.

code

dart · 21 lines
dart
import 'package:flutter/material.dart';

class EpisodeHeader extends StatelessWidget {
  const EpisodeHeader({super.key, required this.onPlay});

  final VoidCallback onPlay;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        // Informative: named for the screen reader.
        Image.asset('assets/artwork.png', width: 64, semanticLabel: 'Episode 12 artwork'),
        // Decorative: skipped entirely.
        Image.asset('assets/wave.png', width: 24, excludeFromSemantics: true),
        // The tooltip becomes the button's accessible name.
        IconButton(icon: const Icon(Icons.play_arrow), tooltip: 'Play', onPressed: onPlay),
      ],
    );
  }
}

go deeper

for a junior

Recall the fixes: tooltip or Icon semanticLabel for icon buttons, Image semanticLabel for meaningful images, excludeFromSemantics for decorative ones.

for a middle

Explain that labels land in the semantics tree, why the role must not be in the label, and how a GestureDetector control gets a name, role and action.

for a senior

Audit a screen with a screen reader and SemanticsDebugger, and add label checks to widget tests so regressions fail before release.

for a principal

Make accessible names part of the component contract, so shared widgets require a label or tooltip instead of relying on each screen.

## What a screen reader actually reads Flutter draws every pixel itself, so TalkBack (Android) and VoiceOver (iOS) cannot inspect native views. Instead the framework builds a parallel **semantics tree**: a tree of `SemanticsNode`s, each carrying a **label** (the name read aloud), optional **value** and **hint**, **flags** (button, image, header, slider, checked, ...) and **actions** (tap, long press, increase, ...). The engine hands that tree to the platform accessibility API. Anything not described there does not exist for a screen-reader user. Most material and cupertino widgets annotate themselves. The gaps appear where a widget has no text: icons, images, and custom controls. ## Icon-only buttons An `IconButton(icon: Icon(Icons.play_arrow), onPressed: ...)` has the button role but no label, so the user hears only "button". Three ways to fix it: 1. `IconButton(tooltip: 'Play', ...)` — the tooltip text becomes the accessible name and also appears on long-press or hover. This is the usual choice. 2. `Icon(Icons.play_arrow, semanticLabel: 'Play')` — the label lives on the icon and is picked up by the button. 3. `Semantics(label: 'Play', child: ...)` around a custom control. Do not put the role in the label ("Play button"): the platform already appends the role, so the user would hear "Play button, button". ## Images: informative versus decorative | Kind of image | What to set | Result | |---|---|---| | Informative (episode artwork, a chart) | `Image(semanticLabel: 'Artwork for episode 12')` | read as an image with that name | | Decorative (background texture, divider art) | `Image(excludeFromSemantics: true)` | skipped entirely | | Decorative subtree of several widgets | `ExcludeSemantics(child: ...)` | the whole subtree is dropped | An image with neither setting is still exposed as an image node without a name, which wastes a swipe and tells the user nothing. ## Custom tappable widgets A `GestureDetector` or `Listener` adds gestures but no role. A card that starts playback when tapped should be wrapped so it has all three parts a screen reader needs: - **name** — `label: 'Play episode 12'`, - **role** — `button: true`, - **action** — `onTap: ...`, so a double-tap with the screen reader on runs it. `InkWell` and the material buttons already provide the tap action; the `Semantics` wrapper is for widgets built from raw gestures or `CustomPaint`. ## Text labels and the spelling trap - `Text('4:32', semanticsLabel: '4 minutes 32 seconds remaining')` replaces what is read for the visible text. Note the **s**: `semanticsLabel`. - `Icon` and `Image` use `semanticLabel`, without the s. - `Semantics` itself uses `label`, plus `hint` for extra guidance read after a pause, and `value` for a current value. Mixing the spellings is a compile error, which at least makes the trap loud. ## Checking the result Turn on TalkBack or VoiceOver and swipe through the screen, or wrap the app in `SemanticsDebugger` (or set `showSemanticsDebugger: true` on `MaterialApp`) to see each node's label drawn over the UI. In widget tests, `find.bySemanticsLabel('Play')` fails when a label is missing, which catches regressions before release.

  • Why shouldn't a Flutter icon button's label include the word 'button'?
    The `IconButton` already sets the button flag in the semantics tree, and TalkBack and VoiceOver announce the role themselves. A label of 'Play button' is read as 'Play button, button'. Keep the label to the action or the object — 'Play' — and let the platform supply the role.
  • In Flutter, what is the difference between Semantics label and hint?
    `label` is the name of the element, read first when it gains accessibility focus. `hint` describes what happens on activation and is read after the label and role, often after a pause, and some users switch hints off. Put essential information in the label and optional guidance in the hint.

saying these in an interview costs you the question

  • Screen readers read the text Flutter paints on the canvas
  • An IconButton is announced by its icon's name automatically
  • Decorative images need a label such as 'decorative image'
  • The label should say 'Play button' so the role is clear
  • Text, Icon and Image all use the same semanticLabel parameter
open as a page

In Flutter, what replaced textScaleFactor, and how should a widget read and apply the user's font size setting?

level: juniorimportance: must knowfreq 52%

basics

~10 s

TextScaler replaced the double textScaleFactor, because Android 14 scales large text less than small text. Text applies MediaQuery.textScalerOf(context) automatically; custom code calls scaler.scale(fontSize) instead of multiplying by a factor.

open as a page

In Flutter 3.47, what does it take to set up gen-l10n so .arb files become an AppLocalizations class wired into MaterialApp?

level: middleimportance: must knowfreq 52%

basics

~10 s

Add flutter_localizations (from the SDK) and intl, set flutter: generate: true in pubspec.yaml, add l10n.yaml and the .arb files, then pass AppLocalizations.localizationsDelegates and AppLocalizations.supportedLocales to MaterialApp. The generated Dart lands in your source tree.

open as a page

In Flutter, what do MergeSemantics and ExcludeSemantics do to the semantics tree, and when do you use each?

level: middleimportance: must knowfreq 45%

basics

~10 s

MergeSemantics folds a subtree's semantics into one node, so a checkbox and its Text are one focus stop read together. ExcludeSemantics removes a subtree from the semantics tree, so screen readers skip it entirely.

open as a page

In a Flutter app using gen-l10n, how do you show a translated string in a widget, and why does AppLocalizations.of(context) usually need a !?

level: juniorimportance: should knowfreq 42%

basics

~20 s

Call AppLocalizations.of(context) from a widget below MaterialApp and read the generated getter or method, such as cartTitle. It returns a nullable type because gen-l10n's nullable-getter option defaults to true, so call sites add ! unless that option is false.

open as a page

In Flutter, what do EdgeInsetsDirectional and AlignmentDirectional do, and why use them instead of EdgeInsets.only(left:) in an app shipping Arabic?

level: juniorimportance: should knowfreq 41%

basics

~20 s

EdgeInsetsDirectional and AlignmentDirectional describe spacing and position with start and end, which Flutter resolves to left or right from the ambient TextDirection at layout. EdgeInsets.only(left:) stays on the left, so it breaks in right-to-left locales.

open as a page

In a Flutter grocery app using gen-l10n, how do you write an .arb plural for cart item counts correct in English, German and Polish?

level: middleimportance: should knowfreq 46%

basics

~20 s

Write an ICU plural such as {count, plural, =0{...} =1{...} other{...}} in each ARB file, giving every language the CLDR categories it needs: one and other for English and German, one, few and many for Polish, always with other.

open as a page

In a Flutter app using package:intl, why can DateFormat.yMMMd() print English dates on an Arabic phone, and how do you fix it?

level: middleimportance: should knowfreq 34%

basics

~10 s

DateFormat without a locale uses Intl.defaultLocale, which Flutter never sets, so it falls back to en_US. Pass the resolved locale, such as Localizations.localeOf(context).toString(), or set Intl.defaultLocale whenever the app locale changes.

open as a page

In Flutter, where does a widget's ambient TextDirection come from, and how do you force a left-to-right island inside an Arabic screen?

level: middleimportance: should knowfreq 36%

basics

~10 s

The ambient direction comes from the nearest Directionality widget, which the app's Localizations widget inserts using WidgetsLocalizations.textDirection. For a left-to-right island, wrap that subtree in Directionality(textDirection: TextDirection.ltr).

open as a page

With package:intl in a Flutter currency converter, how do you format amounts for Arabic and Hebrew users, and how do NumberFormat.currency and simpleCurrency differ?

level: middleimportance: should knowfreq 38%

basics

~20 s

Create NumberFormat.currency(locale: ..., name: 'USD') with the app's resolved locale so the locale decides separators, digits and symbol position. currency prints the ISO code unless given a symbol; simpleCurrency prints a short symbol that can be ambiguous.

open as a page

In Flutter, how do you inspect what the semantics tree will announce, without guessing from the widget code?

level: middleimportance: should knowfreq 28%

basics

~10 s

Overlay SemanticsDebugger (or MaterialApp's showSemanticsDebugger: true) to see each node's label on screen, dump the tree with debugDumpSemanticsTree() or the S key in flutter run, and assert labels in widget tests after tester.ensureSemantics().

open as a page

In Flutter 3.47, how do you mark a section heading and announce a status change, and why is header: true no longer enough?

level: middleimportance: should knowfreq 30%

basics

~10 s

Use Semantics(headingLevel: 1..6) for headings: since Flutter 3.47, header: true is a no-op on iOS and Android. For status changes, mark the text Semantics(liveRegion: true) so updates are announced politely without moving focus.

open as a page

In Flutter, what do TextScaler.clamp and MediaQuery.withClampedTextScaling do, and when is clamping the user's text scale justified?

level: middleimportance: should knowfreq 34%

basics

~10 s

TextScaler.clamp limits scaled text to between minScaleFactor and maxScaleFactor times the font size; MediaQuery.withClampedTextScaling applies that to a subtree. Clamp only locally, such as oversized display text, never the whole app's body text.

open as a page

In Flutter, how do Material widgets guarantee a 48x48 tap target, and what do MaterialTapTargetSize.shrinkWrap and VisualDensity change?

level: middleimportance: should knowfreq 33%

basics

~20 s

Material widgets pad their hit region to kMinInteractiveDimension, 48 by 48 logical pixels, when materialTapTargetSize is padded, the mobile default. shrinkWrap drops that padding, and a negative VisualDensity shrinks the padded minimum by 4 pixels per step.

open as a page

A Flutter grocery app localized with gen-l10n into English, German and Polish shows German to French-speaking users and English strings to some Polish users; what is wrong?

level: seniorimportance: should knowfreq 28%

basics

~20 s

The generated supportedLocales list is alphabetical (de, en, pl), and an unmatched device locale falls back to the first entry, German; set preferred-supported-locales: [en]. English text in Polish means keys missing from app_pl.arb were filled from the template.

open as a page

The Arabic and Hebrew build of a Flutter currency converter shows padding, a gradient and a swap arrow on the wrong side; how do you find and fix these RTL bugs?

level: seniorimportance: should knowfreq 32%

basics

~10 s

Search for absolute geometry (EdgeInsets.only(left:), Alignment.centerLeft, TextAlign.left, BorderRadius topLeft, LinearGradient's centerLeft default) and replace it with directional types; mirror custom icons and painters from Directionality.of, and pin both directions with widget or golden tests.

open as a page

How do you make a custom-painted volume slider in a Flutter podcast player adjustable by TalkBack and VoiceOver users?

level: seniorimportance: should knowfreq 26%

basics

~10 s

Wrap the painted control in Semantics(slider: true, label: 'Volume', value, increasedValue, decreasedValue, onIncrease, onDecrease). Screen readers then announce it as an adjustable slider, and their increase and decrease gestures step the volume.

open as a page

A Flutter train-timetable screen overflows at the largest system font size; how do you reproduce, find and fix the breakages?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Reproduce at the largest font on devices and at a 2.0 test text scale, find the RenderFlex overflows, then drop fixed heights, let text wrap inside Expanded, stack rows into columns when needed, and make the screen scroll.

open as a page

In a Flutter .arb file for gen-l10n, how do placeholders turn a message into a method, and what do type, format and ICU select control?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

A {name} in an ARB message makes gen-l10n generate a method whose parameters are the placeholders. The @key metadata sets each placeholder's Dart type and an intl number or date format; an ICU select picks text by a String value.

open as a page

In a Flutter currency converter supporting en, ar and he, how is the app locale resolved, and when should you write localeListResolutionCallback?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

WidgetsApp tries localeListResolutionCallback, then localeResolutionCallback, then basicLocaleListResolution, moving on whenever a callback returns null. Write the list callback only for a product rule the default cannot express; the single callback sees just the first preferred locale.

open as a page

How do Flutter's meetsGuideline checks for tap targets, labels and text contrast work in a widget test, and what do they miss?

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

After tester.ensureSemantics(), expectLater(tester, meetsGuideline(guideline)) scans the pumped screen's semantics nodes: tap targets against 48x48 or 44x44, tappable nodes for a label, and text for WCAG contrast from sampled pixels. They check one state, so they miss much.

open as a page