Mastering Flutter: Semantics

theotherdevs

TheOtherDev/s

Posted on May 26, 2021

Mastering Flutter: Semantics

Accessibility is a hard subject, as it goes beyond simple Voiceover or Talkback features. It's a combination of UX, UI and good programming. We are not here to tell you how to create a fully accessible Flutter app, but al least how to start it by using Semantics!

Semantics is a powerful widget which adds "features" to a child widget, like setting it as a header, giving it "button" capabilities and tags etc. In this article we'll explore some practical cases and some tips to implement it efficently as it may be... a bit tricky.

Be aware that many widgets already have Semantics functionalities built-in as they have this widget inside so, before wrapping everything around it check out the widgets code and try ot yourself.

Buttons and onTap

The first case we'll consider is to add click functionality and "button" tag to a widget which doesen't come out of the box with this functionality. Let's consider. for example, a simple text, that we wish to make clickable and labelled as a button:

Semantics(
          button: true,
          enabled: true,
          onTap: () {
            print('Clicked!');
          },
          child: Text('Click Me!', style: TextStyle(fontSize: 56)),
        )
Enter fullscreen mode Exit fullscreen mode

This code will make the focused text say "Click Me! button", and absolutely print "Clicked"... Only with screen reader on! Yes, you should not rely on it as a click callback because it will be called only on Voiceover or Talkack click gesture. If you wish to add the click mechanic also without screen readers a good way to do it is to wrap your child around a GestureDetector and replicate the click functionality and get rid completely of the onTap from Semantics:

Semantics(
          button: true,
          enabled: true,
          child: GestureDetector(
            onTap: () {
              print('Clicked!');
            }, child: Text('Click Me!', style: TextStyle(fontSize: 56))
          ),
        )
Enter fullscreen mode Exit fullscreen mode

We can also give our text another different VO label by adding the "label" property:

Semantics(
          button: true,
          enabled: true,
          label: 'Clickable text here!',
          child: GestureDetector(
            onTap: () {
              print('Clicked!');
            }, child: Text('Click Me!', style: TextStyle(fontSize: 56))
          ),
        )
Enter fullscreen mode Exit fullscreen mode

With screen readers activated, our text will be read: "Clickable text here, Cick me! button".

ExcludeSemantics

Another useful widget is ExcludeSemantics, which drops all the semantics data from its child on. Let's see how. Let's consider the example above (the giant "CLICK ME" text). Now screen readers will read "Clickable text here, Cick me! button". Wrapping ExcludeSemantics arounf the Semantics element:

ExcludeSemantics(
          excluding: true,
          child: Semantics(
            button: true,
            enabled: true,
            label: 'Clickable text here!',
            child: GestureDetector(
              onTap: () {
                print('Clicked!');
              }, child: Text('Click Me!', style: TextStyle(fontSize: 56))
            ),
          ),
        )
Enter fullscreen mode Exit fullscreen mode

will prevent readers to focus on the text. This may be useful if some widget should not be considered in some situations, so we'll set "excluding" on true, by setting on false the widget will work as before.

Now, we want to "fix" our text by only making readers say "Clickable text here button", so by dropping the "Click me" added by the Text child. Nothing easier, let's just wrap the ExcludeSemantics widget around the Text Widget:

Semantics(
          button: true,
          enabled: true,
          label: 'Clickable text here!',
          child: GestureDetector(
            onTap: () {
              print('Clicked!');
            }, child: ExcludeSemantics(
              excluding: true,
              child: Text('Click Me!', style: TextStyle(fontSize: 56))
            )
          ),
        )
Enter fullscreen mode Exit fullscreen mode

there's also an easier way to do it. Semantics has an awesome excludeSemantics property which does exactly the same as wrapping ExcludeSemantics:

Semantics(
          button: true,
          enabled: true,
          excludeSemantics: true,
          label: 'Clickable text here!',
          child: GestureDetector(
            onTap: () {
              print('Clicked!');
            }, child: Text('Click Me!', style: TextStyle(fontSize: 56))
          ),
        )
Enter fullscreen mode Exit fullscreen mode

The codes above give the same result, with the only exception that the excludeSemantics property will drop Seamantics data from all children of the Semantics widget, so if you only wish to drop some of the children in a way more complex widget tree you should use the ExcludeSemantics widget.

MergeSemantics

Let's now consider a more complex situation, like a column with 2 texts that we wish the readers to read as one:

Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text('I am a', style: TextStyle(fontSize: 56)),
              Text('Text!', style: TextStyle(fontSize: 46)),
            ],
          )
Enter fullscreen mode Exit fullscreen mode

this code will let the reader read "I am a" and "Text!" separately.

To "merge" the two widgets together in a unique "I am a Text!" we'll need to use... MergeSemantics (ba dum chhh!)! MergeSemantics is a simple widget that will unite all Semantics data:

MergeSemantics(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text('I am a', style: TextStyle(fontSize: 56)),
              Text('Text!', style: TextStyle(fontSize: 46)),
            ],
          ),
        )
Enter fullscreen mode Exit fullscreen mode

this code will let Voiceover/Talkback focus on both texts at once and read "I am a Text!".

Want to add more flavour to our text? Let's wrap the second one around a Semantics widget! By merging all Semantics data it will result in VO saying: "I am a wonderful text!"

MergeSemantics(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text('I am a', style: TextStyle(fontSize: 56)),
              Semantics(
                label: 'Wonderful',
                child: Text('Text!', style: TextStyle(fontSize: 46))
              ),
            ],
          ),
        )
Enter fullscreen mode Exit fullscreen mode

Where to go now?

That's just the beginning to the creation of better, more accessibile, apps. If you wish to dive more into this subject check out this series of guidelines from Level Access and enlarge your userbase, while doing something good for the others!

Written by the awesome... me 😊!!

💖 💪 🙅 🚩
theotherdevs
TheOtherDev/s

Posted on May 26, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related