skip to content

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