Flutter includes widgets and APIs that help developers build accessible applications. To make apps ADA-compliant, these widgets should be used correctly along with practices like adding semantic labels, enabling keyboard navigation, and supporting screen readers.

Semantics

The Semantics widget provides information about UI elements to assistive technologies such as screen readers. Without semantics, a button may only be announced as “button.” With semantics, it can be described more clearly, for example: “Submit form, double-tap to submit.”

This widget allows developers to define what an element is, what it does, and what state it is in. That way, screen reader users receive the same meaningful context that sighted users see visually.

code
Semantics(
code
label: 'Submit form',
code
hint: 'Double tap to submit',
code
button: true,
code
child: ElevatedButton(
code
onPressed: () {},
code
child: Text('Submit'),
code
),
code
);

Focus and FocusTraversalGroup

Once widgets have semantic meaning, you need to ensure users can actually move between them without touch. That’s where Focus and FocusTraversalGroup come in.

These widgets let users navigate UI elements using a keyboard, D-pad, or remote control. They ensure that your semantics-labeled elements are not just read out, but also reachable.

So after labeling elements with Semantics, you handle how users move across them with focus management.

code
FocusTraversalGroup(
code
child: Column(
code
children: [
code
Focus(
code
child: TextField(),
code
),
code
Focus(
code
child: ElevatedButton(onPressed: () {}, child: Text('Next')),
code
),
code
],
code
),
code
);

Shortcuts and Actions

After focus navigation is working, you improve usability with shortcuts. For example, pressing Enter to activate a button or arrow keys to move between options.

Shortcuts and Actions connect keyboard input to widget behavior. This makes your app predictable for people who cannot use touch gestures

code
Shortcuts(
code
shortcuts: {
code
LogicalKeySet(LogicalKeyboardKey.enter): ActivateIntent(),
code
},
code
child: Actions(
code
actions: {
code
ActivateIntent: CallbackAction<ActivateIntent>(
code
onInvoke: (_) => debugPrint('Enter pressed'),
code
),
code
},
code
child: Focus(
code
child: ElevatedButton(onPressed: () {}, child: Text('OK')),
code
),
code
),
code
);

ExcludeSemantics and MergeSemantics

Sometimes, too much information is read aloud, which confuses users. These widgets let you control how screen readers read groups of widgets.

For example, instead of “Volume icon, 80%” being read in two pieces, MergeSemantics lets it be read as “Volume 80%.” This refines the information from the Semantics so it sounds natural.

code
MergeSemantics(
code
child: Row(
code
children: [
code
Icon(Icons.volume_up),
code
Text('Volume 80%'),
code
],
code
),
code
);

Tooltip

Tooltips give extra hints. Sighted users see them on hover/focus, and screen readers announce them. For example, an icon-only trash button might be unclear, but adding a tooltip makes it read as “Delete item.” This complements semantics by adding extra clarity where visuals alone aren’t enough.

code
Tooltip(
code
message: 'Delete item',
code
child: IconButton(
code
icon: Icon(Icons.delete),
code
onPressed: () {},
code
),
code
);

MediaQuery and Accessibility Settings

Users can change global accessibility preferences (large text, reduce motion, high contrast). MediaQuery allows your app to respect these preferences automatically.

For example, if someone enables larger fonts in their phone settings, your app should scale text instead of cutting it off. This ensures your app doesn’t fight user preferences.

code
final scale = MediaQuery.of(context).textScaleFactor;

ElevatedButton, Switch, Checkbox (Pre-Built Accessible Widgets)

Flutter’s built-in Material widgets already include accessibility roles and states. For example, a Checkbox automatically tells the screen reader whether it’s checked or not.

This is the default layer of accessibility you get for free if you use Flutter’s standard widgets. But you still need to wrap them in Semantics or ListTile to give them context.

code
Checkbox(
code
value: isChecked,
code
onChanged: (val) {},
code
)