flutter_a11y_lens 0.0.1 copy "flutter_a11y_lens: ^0.0.1" to clipboard
flutter_a11y_lens: ^0.0.1 copied to clipboard

Live accessibility auditing for Flutter: inspects the running widget tree and flags WCAG violations in real time, with an on-screen debug overlay.

flutter_a11y_lens #

Live accessibility auditing for Flutter: inspects the running widget tree and flags accessibility violations in real time, with an on-screen debug overlay — runtime inspection rather than static lint rules.

Wrap your app in one widget. It walks the tree after every frame, runs WCAG-based checks, and draws an outline over anything that fails. Tap an outline to see the rule, the message, and a suggested fix. The same checks also run headless in flutter test, so a CI build can fail on new violations without a device or screen reader.

See it in action #

The example/ app demonstrates all four rules with a live before/after toggle — the screenshots below are that app, running on iOS.

The example app listing four demo cards, one per rule (ContrastRule, TapTargetSizeRule, SemanticLabelRule, FocusOrderRule), each with a colored outline drawn over its deliberate violation

Tap any outline for its detail popover:

Contrast Semantic label Focus order
Popover for the contrast rule: "Text contrast ratio 1.36:1 is below the 4.5:1 minimum for normal text." with a suggested fix to darken the text or lighten the background Popover for the semantic-label rule: "Interactive element has no label, value, or tooltip for a screen reader to announce." with a suggested fix to add a Semantics label Popover for the focus-order rule: "Focus order among 2 sibling widgets does not match their visual layout." with a suggested fix to reorder the widgets or set an explicit sort key

Features #

  • Zero setup: one widget wraps your app; no per-widget annotations required for the built-in rules to work.
  • Reads what assistive technology actually reads: contrast and tap-target checks walk the Element/render tree; semantic-label and focus-order checks walk the real SemanticsNode tree the engine hands to screen readers, so they catch widgets that set semantics via a custom RenderObject (IconButton, InkWell, ...), not just an explicit Semantics widget.
  • Live overlay: colored outlines by severity, tap-to-inspect popover with a concrete suggested fix, updates every frame.
  • CI-ready: the same rules run headless under flutter test — no device, simulator, or screen reader needed — with baseline diffing so a build only fails on new issues.
  • Extensible: A11yRule / A11ySemanticsRule are public interfaces; register your own alongside or instead of the built-ins for team-specific design-system tokens (a custom minimum tap size, a stricter contrast ratio, ...).
  • Zero cost in release builds: the overlay is a no-op behind a compile-time kReleaseMode check — the walkers, rules, and overlay widgets are compiled away entirely, not just hidden.

Installation #

flutter pub add flutter_a11y_lens

Or add it manually to pubspec.yaml:

dependencies:
  flutter_a11y_lens: ^0.0.1

Quick start: the live overlay #

Wrap the root of your app:

import 'package:flutter/material.dart';
import 'package:flutter_a11y_lens/a11y_lens.dart';

void main() {
  runApp(A11yLensOverlay(child: const MyApp()));
}

That's the whole setup. In debug/profile builds, A11yLensOverlay:

  1. Walks the widget tree below child after every frame.
  2. Runs the registered rules (contrast + tap-target-size against the Element tree; semantic-label + focus-order against the SemanticsNode tree) against the snapshot.
  3. Draws a colored outline over every violation — red for A11ySeverity.error, amber for A11ySeverity.warning, blue for A11ySeverity.info.
  4. Shows a detail popover (rule, message, suggested fix) when you tap an outline.

In release builds, A11yLensOverlay.build returns child unchanged; the kReleaseMode check is a compile-time constant, so the Dart compiler strips the walkers, rules, and overlay widgets from the release binary rather than just disabling them at runtime.

Options #

A11yLensOverlay(
  enabled: kDebugMode, // or any other runtime flag
  rules: const [ContrastRule(), TapTargetSizeRule()],       // Element-tree rules
  semanticsRules: const [SemanticLabelRule(), FocusOrderRule()], // SemanticsNode-tree rules
  child: const MyApp(),
)
  • enabled: false disables auditing without removing the widget (e.g. from a debug settings screen). Has no effect in release builds, which are always disabled.
  • rules / semanticsRules accept custom implementations of A11yRule / A11ySemanticsRule — add your own or drop the defaults. Pass semanticsRules: const [] to skip semantics auditing entirely, avoiding the SemanticsHandle it requires (a small but real, debug-only, performance cost).

Rules #

Rule id Severity Tree Checks
ContrastRule contrast error Element Text color vs. the nearest solid ancestor background (ColoredBox, Container, DecoratedBox, Material, Scaffold) meets WCAG AA (default) or AAA contrast.
TapTargetSizeRule tap-target-size warning Element Interactive widgets (GestureDetector, IconButton, ElevatedButton, InkWell, ...) are at least 48x48dp.
SemanticLabelRule semantic-label error Semantics Interactive semantics nodes (tap/long-press action) have a label, value, or tooltip.
FocusOrderRule focus-order warning Semantics Sibling interactive nodes are traversed in the same order they're laid out visually (top-to-bottom, left-to-right).

Every rule accepts overrides — e.g. ContrastRule(level: A11yContrastLevel.aaa) or TapTargetSizeRule(minimumSize: 44) for platform-specific minimums — and ColorContrast exposes the underlying luminance/contrast-ratio math standalone if you need it elsewhere.

Writing a custom rule #

class NoEmojiInLabelsRule extends A11ySemanticsRule {
  const NoEmojiInLabelsRule();

  @override
  String get id => 'no-emoji-in-labels';

  @override
  String get description => 'Semantic labels should not contain emoji.';

  @override
  List<A11yViolation> check(List<A11ySemanticsNodeInfo> nodes) {
    return [
      for (final node in nodes)
        if (_containsEmoji(node.label))
          A11yViolation(
            ruleId: id,
            severity: A11ySeverity.warning,
            bounds: node.rect,
            message: 'Label "${node.label}" contains emoji, which some '
                'screen readers announce verbosely.',
          ),
    ];
  }
}

Register it via A11yLensOverlay(semanticsRules: [...defaults, NoEmojiInLabelsRule()]).

CI: fail the build on violations #

package:flutter_a11y_lens/testing.dart — a separate import so apps that don't test with it aren't pulled into a flutter_test dependency — adds expectNoA11yViolations, a headless flutter test harness:

import 'package:flutter/material.dart';
import 'package:flutter_a11y_lens/testing.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:my_app/main.dart';

void main() {
  testWidgets('home screen has no accessibility violations', (tester) async {
    await tester.pumpWidget(const MyApp());
    await expectNoA11yViolations(tester);
  });
}

No device, simulator, or screen reader needed — ElementTreeWalker and SemanticsTreeWalker both read data the engine already computes during a normal widget-test pump (testWidgets enables semantics by default).

Useful options:

await expectNoA11yViolations(
  tester,
  finder: find.byKey(const Key('checkoutScreen')), // scope to a subtree
  minimumSeverity: A11ySeverity.error,              // ignore warnings
  baseline: baselineReport,                         // only fail on *new* issues
);

Failing only on new violations #

Check in a baseline report, then diff against it in CI:

// One-time / on demand: capture the current state as a baseline.
final report = A11yReport.generate(context);
File('a11y-baseline.json').writeAsStringSync(report.toJsonString());
// In CI: only fail on violations not already in the baseline.
final baseline = A11yReport.fromJson(
  jsonDecode(File('a11y-baseline.json').readAsStringSync()),
);
await expectNoA11yViolations(tester, baseline: baseline);

newViolationsComparedTo matches on rule + widget type + message, not exact pixel bounds — bounds can legitimately shift between runs (different viewport, unrelated layout changes) without the underlying issue being new.

Programmatic API #

For anything else — a report to disk, a Markdown comment on a PR, a custom CI step — use A11yReport.generate directly, or the walkers and rules it's built from:

final report = A11yReport.generate(context); // default rules
print(report.toMarkdown());
File('a11y-report.json').writeAsStringSync(report.toJsonString());
const walker = ElementTreeWalker();
final elements = walker.walk(context);

const semanticsWalker = SemanticsTreeWalker(); // needs an active SemanticsHandle
final semanticsNodes = semanticsWalker.walk(context);

final violations = [
  ...const ContrastRule().check(elements),
  ...const TapTargetSizeRule().check(elements),
  ...const SemanticLabelRule().check(semanticsNodes),
  ...const FocusOrderRule().check(semanticsNodes),
];

Example app #

example/ is a runnable app with one deliberate violation per rule, each in its own card with a "Show fixed version" toggle so you can compare the before/after state live (the screenshots above are this app). Run it with:

cd example
flutter run

example/test/a11y_test.dart and example/test/widget_test.dart show the CI harness both catching the deliberate violations and confirming the toggle genuinely fixes them.

License #

MIT — see LICENSE.

1
likes
160
points
69
downloads

Documentation

API reference

Publisher

verified publisherehsanur.com

Weekly Downloads

Live accessibility auditing for Flutter: inspects the running widget tree and flags WCAG violations in real time, with an on-screen debug overlay.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, flutter_test

More

Packages that depend on flutter_a11y_lens