Skip to main content

Web accessibility

Information about web accessibility

Background

#

Flutter supports web accessibility by translating its internal Semantics tree into an accessible HTML DOM structure that screen readers can understand. Since Flutter renders its UI on a single canvas, it needs a special layer to expose the UI's meaning and structure to web browsers.

Opt-in web accessibility

#

Invisible button

#

For performance reasons, Flutter's web accessibility is not on by default. To turn on accessibility, the user needs to press an invisible button with aria-label="Enable accessibility". After pressing the button, the DOM tree will reflect all accessibility information for the widgets. In embedded or multi-view web apps on mobile browsers, Flutter scopes this placeholder button to each Flutter view so it doesn't block taps on the surrounding HTML page.

Turn on accessibility mode in code

#

An alternative approach is to turn on accessibility mode by adding the following code when running an app.

dart
import 'package:flutter/semantics.dart';
​
void main() {
  runApp(const MyApp());
  if (kIsWeb) {
    SemanticsBinding.instance.ensureSemantics();
  }
}

Enhancing Accessibility with Semantic Roles

#

What are Semantic Roles?

#

Semantic roles define the purpose of a UI element, helping screen readers and other assistive tools interpret and present your application effectively to users. For example, a role can indicate whether a widget is a button, a link, a heading, a slider, or part of a table.

While Flutter's standard widgets often provide these semantics automatically, a custom component without a clearly defined role can be incomprehensible to a screen reader user.

By assigning appropriate roles, you ensure that:

  • Screen readers can announce the type and purpose of elements correctly.
  • Users can navigate your application more effectively using assistive technologies.
  • Your application adheres to web accessibility standards, improving usability.

Using SemanticsRole in Flutter for web

#

Flutter provides the Semantics widget with the SemanticsRole enum to allow developers to assign specific roles to their widgets. When your Flutter web app is rendered, these Flutter-specific roles are translated into corresponding ARIA roles in the web page's HTML structure.

1. Automatic Semantics from Standard Widgets

Many standard Flutter widgets, like TabBar, MenuAnchor, Slider, RangeSlider, and Table, automatically include semantic information along with their roles:

  • Sliders: Slider and RangeSlider (using SemanticsFlag.isSlider) and custom widgets with SemanticsRole.slider render a focusable <input type="range" role="slider"> element with synchronized aria-label, aria-valuenow, aria-valuetext, and keyboard arrow navigation.
  • Landmarks and menus: Named routes with scopesRoute: true, namesRoute: true, and a non-empty label emit role="region" landmarks, and scrollable menus (MenuAnchor, DropdownMenu) expose direct item counts to screen readers without intermediate group wrappers.

In addition, the web engine preserves DOM focus across semantic role changes without scrolling, restores focus when dialogs or modal routes close in WidgetsApp.router apps, and dismisses visible Tooltip widgets when the user presses Escape.

Whenever possible, prefer using these standard widgets as they handle many accessibility aspects out-of-the-box.

2. Explicitly adding or overriding roles

For custom components or when the default semantics aren't sufficient, use the Semantics widget to define the role:

Here's an example of how you might explicitly define a list and its items:

dart
import 'package:material_ui/material_ui.dart';
import 'package:flutter/semantics.dart';
​
​
class MyCustomListWidget extends StatelessWidget {
  const MyCustomListWidget({Key? key}) : super(key: key);
​
  @override
  Widget build(BuildContext context) {
    // This example shows how to explicitly assign list and listitem roles
    // when building a custom list structure.
    return Semantics(
      role: SemanticsRole.list,
      explicitChildNodes: true,
      child: Column(
        children: <Widget>[
          Semantics(
            role: SemanticsRole.listItem,
            child: const Padding(
              padding: EdgeInsets.all(8.0),
              child: Text('Content of the first custom list item.'),
            ),
          ),
          Semantics(
            role: SemanticsRole.listItem,
            child: const Padding(
              padding: EdgeInsets.all(8.0),
              child: Text('Content of the second custom list item.'),
            ),
          ),
        ],
      ),
    );
  }
}

Viewport zoom and mobile text inputs

#

By default in full-page mode, Flutter configures the page's <meta name="viewport" flt-viewport> tag with maximum-scale=5.0 and without user-scalable=no. This allows users to pinch-to-zoom up to 500% to meet WCAG 2.1 AA text-resizing guidelines.

On iOS Safari, Flutter also automatically sets a minimum 16px font size on its hidden semantic <input> and <textarea> elements so the browser doesn't unexpectedly auto-zoom when a user focuses a text field.