Keyboard Accessibility in Flutter: Is your application ready?

Keyboard Accessibility in Flutter: Is your application ready?

FlutterPulse

This article was translated specially for the channel FlutterPulseYou'll find lots of interesting things related to Flutter on this channel. Don't hesitate to subscribe!🚀

Master Flutter's focus system to build accessible apps with seamless keyboard navigation for all users.

Accessibility is not a feature; it's a right.

Even small adjustments, such as correct keyboard navigation, can make a huge difference for disabled or special needs users. Flutter offers means, such as FocusTraversalGroup, FocusTraversalOrder, ExcludeFocus and FocusNode, for keyboard-driven navigation that is deterministic and comfortable to use.

In this article, we'll explore how to implement keyboard accessibility using these widgets, customize focus order, and ensure your app is inclusive and accessible to all.

Why accessibility matters

Think of a login interface where the user is able to browse the interface using just a keyboard. To most of us, it may appear like a trivial one.

However, for a person with motor impairments, whose method of interaction with a computer system is driven by physical keyboard input or assistive technology (e.g., screen reader), logical focus navigation is critical.

Keyboard accessibility benefits

  • Individuals with mobility disabilities, who are unable to operate a mouse or touchscreen.
  • Blind or visually handicapped users, who use focus indicators for navigation.
  • Power users, who prefer keyboard shortcuts for faster interaction.

If focus management is not carefully managed, the user can become disoriented in the UI, focus may appear to jump in a random manner, and in the worst case — be completely locked in to the inaccessible parts of the app.

Flutter's focus system allows you to:

  • Group and order focusable widgets.
  • Exclude widgets from the focus tree.
  • Customize how the focus moves between widgets.

Flutter's focus system overview

Flutter provides a hierarchical focus tree, where FocusNode and related widgets manage the focus state of each widget. Here's how the main components work

1. FocusNode

FocusNode is a low-level object which is responsible for monitoring and manipulating the focus of a single widget.

Use cases:

  • Manage keyboard input for a TextField.
  • Control focus manually (e.g., move focus when a user presses "Tab").
  • Detect when a widget gains or loses focus.

Example:

final FocusNode focusNode = FocusNode();

@override
Widget build(BuildContext context) {
return TextField(
focusNode: focusNode,
decoration: InputDecoration(labelText: 'Enter your name'),
);
}

2. FocusTraversalGroup

FocusTraversalGroup clusters widgets into a focus traversal scope and controls the way focus jumps from widgets to each other. This ensures focus order is logical and customizable within that group.

Policies In order to define how focus moves, we need to implement a policy:

  • WidgetOrderTraversalPolicy (Default). Focus order follows the widget tree hierarchy. Is great for most UIs where widgets are arranged logically.
  • ReadingOrderTraversalPolicy. Follows the visual order (left-to-right, top-to-bottom). Is ideal for UIs that resemble a document or form layout.
  • OrderedTraversalPolicy. This one uses FocusTraversalOrder to define a custom focus sequence. Is the most adaptive in complex layouts where the default order is not logical.

Example:

FocusTraversalGroup(
policy: WidgetOrderTraversalPolicy(),
child: Column(
children: [
TextField(decoration: InputDecoration(labelText: 'Username')),
TextField(decoration: InputDecoration(labelText: 'Password')),
ElevatedButton(onPressed: () {}, child: Text('Login')),
],
),
);

Here, FocusTraversalGroup keeps focus in the order in which widgets are defined.

3. FocusTraversalOrder

FocusTraversalOrder customizes the focus sequence inside a FocusTraversalGroup. This is particularly relevant if you wish the focus to be directed in order and that does not correspond to the widget hierarchy.

There are different order types:

  • NumericFocusOrder. Focuses widgets in ascending numerical order.
  • LexicalFocusOrder. Focuses widgets alphabetically (by String order).

Example:

FocusTraversalGroup(
policy: OrderedTraversalPolicy(), // Uses the NumericFocusOrder
child: Column(
children: [
FocusTraversalOrder(
order: NumericFocusOrder(2),
child: TextField(decoration: InputDecoration(labelText: 'Password')),
),
FocusTraversalOrder(
order: NumericFocusOrder(1),
child: TextField(decoration: InputDecoration(labelText: 'Username')),
),
FocusTraversalOrder(
order: NumericFocusOrder(3),
child: ElevatedButton(onPressed: () {}, child: Text('Login')),
),
],
),
);

In this example, the focus order will be: Username > Password > Login Button

If FocusTraversalOrder is not used, the focus will be determined by the sequence of the widget tree, which might be at odds with the intended navigation flow.

4. ExcludeFocus

ExcludeFocus temporarily "lifts" the widget and its descendants out of the focus tree. This is useful, for example, for concealing or disabling parts of the UI without breaking keyboard navigation.

ExcludeFocus(
excluding: true,
child: ElevatedButton(onPressed: () {}, child: Text('Disabled Button')),
);

Here, in this case, the button is unfocusable, that is, actually a button in the traditional sense is not focusable, thus users will not be able to access it by means of the keyboard.

Real-world example: Accessible login form

Let's assemble all the pieces and create an entirely accessible login page with the use of FocusTraversalGroup, FocusTraversalOrder, and a FocusNode.

class AccessibleLoginForm extends StatelessWidget {
final FocusNode usernameFocus = FocusNode();
final FocusNode passwordFocus = FocusNode();
final FocusNode loginButtonFocus = FocusNode();

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Accessible Login')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FocusTraversalOrder(
order: NumericFocusOrder(1),
child: TextField(
focusNode: usernameFocus,
decoration: InputDecoration(labelText: 'Username'),
),
),
FocusTraversalOrder(
order: NumericFocusOrder(2),
child: TextField(
focusNode: passwordFocus,
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
),
),
FocusTraversalOrder(
order: NumericFocusOrder(3),
child: ElevatedButton(
focusNode: loginButtonFocus,
onPressed: () {
// Handle login
},
child: Text('Login'),
),
),
],
),
),
),
);
}
}

Accessibility testing tips

To ensure your app is accessible:

  • Test with a keyboard. Can you navigate through all focusable widgets?
  • Check focus indicators. Is the currently focused widget visually obvious?
  • Avoid focus traps. Ensure users can escape hidden or disabled elements.
  • Test with real users. Get feedback from users with disabilities.

Conclusion

Accessibility is a crucial part of app development. With the focus system of Flutter, keyboard navigation can be implemented effortlessly to make your app accessible and usable for everyone.

By using FocusTraversalGroup, FocusTraversalOrder, and related widgets, you can create predictable, intuitive focus behavior that works for everyone.

Liking this? Follow RaĂşl Ferrer on Medium (and, better, subscribe me) for more insights and inspiration!

And remember to subscribe my newsletter for insightful content about code, lead and grow.

Code, Lead, Grow | RaĂşl Ferrer | Substack

The newsletter will position you as a thought leader, offering a mix of personal stories, practical advice, and…

substack.com

Report Page