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.
Tap any outline for its detail popover:
| Contrast | Semantic label | Focus order |
|---|---|---|
![]() |
![]() |
![]() |
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 realSemanticsNodetree the engine hands to screen readers, so they catch widgets that set semantics via a customRenderObject(IconButton,InkWell, ...), not just an explicitSemanticswidget. - 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/A11ySemanticsRuleare 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
kReleaseModecheck — 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:
- Walks the widget tree below
childafter every frame. - Runs the registered rules (contrast + tap-target-size against the
Elementtree; semantic-label + focus-order against theSemanticsNodetree) against the snapshot. - Draws a colored outline over every violation — red for
A11ySeverity.error, amber forA11ySeverity.warning, blue forA11ySeverity.info. - 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: falsedisables auditing without removing the widget (e.g. from a debug settings screen). Has no effect in release builds, which are always disabled.rules/semanticsRulesaccept custom implementations ofA11yRule/A11ySemanticsRule— add your own or drop the defaults. PasssemanticsRules: const []to skip semantics auditing entirely, avoiding theSemanticsHandleit 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.


