← Articles

Web Accessibility for Mobile App Developers

By Mark · 29 June 20260 views

Web Accessibility for Mobile App Developers

Accessibility is often treated as a checkbox item that gets added at the end of a project, if at all. This is a mistake — both ethically and practically. Globally, over one billion people live with some form of disability. Accessible apps reach more users, perform better in app store rankings (App Store search rewards accessibility), and often indicate higher overall code quality. This guide covers accessibility from the perspective of a Flutter developer, with principles that transfer to any mobile platform.

Why Accessibility Matters in Flutter

Flutter renders its own pixels — it does not use native platform widgets. This means platform accessibility features (VoiceOver on iOS, TalkBack on Android) need to be explicitly integrated through Flutter's Semantics layer. The good news is that most Flutter widgets (Text, ElevatedButton, Checkbox, etc.) include semantics automatically. The bad news is that custom widgets, images, and interactive components often need manual annotation.

The Semantics Widget

The Semantics widget annotates any part of the widget tree with accessibility information:

Semantics(
  label: 'Profile photo for Alice Johnson',
  hint: 'Double tap to edit profile photo',
  button: true,
  child: GestureDetector(
    onTap: onEditProfile,
    child: CircleAvatar(backgroundImage: NetworkImage(photoUrl)),
  ),
)

Without the Semantics wrapper, VoiceOver reads this as "image" with no useful information. With it, the screen reader says "Profile photo for Alice Johnson, button. Double tap to edit profile photo."

Key Semantic Properties

  • label — the name of the element read by screen readers
  • hint — describes what happens when the user interacts (double tap, swipe)
  • value — current value of a slider, progress bar, or text field
  • button — marks the element as a button (screen readers announce "button" after the label)
  • checked / selected — state for checkboxes, radio buttons, tabs
  • enabled — whether the control is interactive
  • onTap, onLongPress — semantic actions the screen reader can invoke

Merge and Exclude Semantics

MergeSemantics combines the semantics of multiple children into a single node — useful for list items that have several components (icon, title, subtitle) but should be read as one element:

MergeSemantics(
  child: Row(
    children: [
      const Icon(Icons.email),
      const SizedBox(width: 8),
      Text(email),
    ],
  ),
)

VoiceOver now reads this as a single item ("email [email protected]") rather than "image" and then the email separately.

ExcludeSemantics hides decorative elements from the accessibility tree:

ExcludeSemantics(
  child: Icon(Icons.star, color: Colors.amber), // Decorative
)

Color Contrast

The WCAG 2.1 guidelines require a minimum contrast ratio of 4.5:1 for normal text and 3:1 for large text. Many beautiful designs fail this test — light gray text on white backgrounds is a very common violation.

Check contrast ratios during design (Figma has built-in contrast checkers, as does the Chrome DevTools accessibility panel for web-based design tools). Do not wait until implementation to discover that your text color does not meet the minimum.

In Flutter, test your theme's text colors against background colors before shipping.

Touch Target Size

Apple's Human Interface Guidelines recommend a minimum touch target of 44×44 points. Google's Material Design recommends 48×48 dp. Small targets are hard to tap for users with motor impairments and frustrating for everyone.

Ensure your interactive elements are at least 44×44 logical pixels:

SizedBox(
  width: 44,
  height: 44,
  child: IconButton(
    icon: const Icon(Icons.close, size: 20),
    onPressed: onClose,
  ),
)

Text Scaling

Flutter respects the system text size setting, but many custom layouts break when the text scale factor is increased to 1.5x or 2x. Test your screens with MediaQuery.of(context).textScaleFactor at 1.0, 1.5, and 2.0.

Avoid hardcoded container heights that assume a specific text height. Use FittedBox or dynamic sizing with Flexible/Expanded so text has room to grow.

Testing Accessibility

  • Manual testing: Enable VoiceOver (iOS) or TalkBack (Android) and navigate through your app using only the screen reader. Does the flow make sense?
  • Flutter accessibility checker: Add flutter_accessibility_service in tests to programmatically check the semantics tree
  • Screen reader order: Ensure the focus order follows a logical top-to-bottom, left-to-right reading order
// Check semantic label in widget tests
expect(
  tester.getSemantics(find.byType(CircleAvatar)),
  matchesSemantics(label: 'Profile photo for Alice Johnson'),
);

Focus Management

When a dialog opens, focus should move to it. When a dialog closes, focus should return to the element that triggered it. Flutter handles this automatically for built-in showDialog and showModalBottomSheet, but custom overlays need explicit focus management with FocusNode and FocusScope.

Conclusion

Accessibility in Flutter is primarily about the Semantics layer — making sure every meaningful UI element has a label, hint, and role. Beyond semantics, check color contrast, touch target size, and text scaling. Test with a real screen reader at least once per release cycle. The investment is modest, the impact for users who rely on these features is profound, and the discipline of thinking about semantics often surfaces interface design issues that benefit all users.

Sign in to like, dislike, or report.

Comments

No comments yet. Be the first!

Sign in to leave a comment.