In Flutter, how do you give an icon-only button and a decorative image the right screen-reader announcement?
answer
- the semantics tree, not the pixels
- IconButton tooltip becomes the label
- semanticLabel on Icon and Image
- excludeFromSemantics for decoration
- Text spells it semanticsLabel
basics
~10 sGive 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 sScreen 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 linesimport '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
Recall the fixes: tooltip or Icon semanticLabel for icon buttons, Image semanticLabel for meaningful images, excludeFromSemantics for decorative ones.
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.
Audit a screen with a screen reader and SemanticsDebugger, and add label checks to widget tests so regressions fail before release.
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