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.

example/lib/main.dart

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

void main() {
  // Wrapping the app in A11yLensOverlay is all it takes: it walks the tree
  // after every frame and draws an outline over anything the rules below
  // flag. Tap an outline to see the rule, message, and suggested fix.
  runApp(const A11yLensOverlay(child: MyApp()));
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  // Flips every card below between "before" (one deliberate violation each,
  // for the overlay/rules to catch) and "after" (the same layout, fixed) —
  // a live before/after comparison you can record straight from this screen.
  bool _fixed = false;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(title: const Text('flutter_a11y_lens example')),
        body: Builder(
          builder: (context) {
            return ListView(
              padding: const EdgeInsets.all(16),
              children: [
                SwitchListTile(
                  title: const Text('Show fixed version'),
                  subtitle: Text(
                    _fixed
                        ? 'After — no violations below'
                        : 'Before — 4 deliberate violations below, one per rule',
                  ),
                  value: _fixed,
                  onChanged: (value) => setState(() => _fixed = value),
                ),
                const SizedBox(height: 8),
                // Scoped separately from the switch/button above and below
                // (both ordinary Material widgets outside this demo's
                // scope) so a violation check can target just the four
                // deliberate issues below.
                Column(
                  key: const Key('a11yDemo'),
                  children: [
                    _DemoCard(
                      number: 1,
                      rule: 'ContrastRule',
                      description:
                          'Text color too close to its background — below '
                          'the 4.5:1 WCAG AA minimum for normal text.',
                      child: _contrastDemo(_fixed),
                    ),
                    _DemoCard(
                      number: 2,
                      rule: 'TapTargetSizeRule',
                      description:
                          'Interactive widget smaller than 48x48dp. Already '
                          'labeled, so only this rule fires — not '
                          'SemanticLabelRule too.',
                      child: _tapTargetSizeDemo(_fixed),
                    ),
                    _DemoCard(
                      number: 3,
                      rule: 'SemanticLabelRule',
                      description:
                          'Interactive widget with no label for a screen '
                          'reader to announce. Already 48x48, so only this '
                          'rule fires — not TapTargetSizeRule too.',
                      child: _semanticLabelDemo(_fixed),
                    ),
                    _DemoCard(
                      number: 4,
                      rule: 'FocusOrderRule',
                      description:
                          'Built (and thus traversed) right-then-left, but '
                          'laid out left-then-right — a screen reader or '
                          'switch control would visit them out of order.',
                      child: _focusOrderDemo(_fixed),
                    ),
                  ],
                ),
                const SizedBox(height: 8),
                ElevatedButton(
                  onPressed: () {
                    const walker = ElementTreeWalker();
                    final elements = walker.walk(context);
                    const semanticsWalker = SemanticsTreeWalker();
                    final semanticsNodes = semanticsWalker.walk(context);

                    final violations = [
                      ...const ContrastRule().check(elements),
                      ...const TapTargetSizeRule().check(elements),
                      ...const SemanticLabelRule().check(semanticsNodes),
                      ...const FocusOrderRule().check(semanticsNodes),
                    ];
                    debugPrint(
                      'flutter_a11y_lens: walked ${elements.length} elements, '
                      'found ${violations.length} violations',
                    );
                    for (final violation in violations) {
                      debugPrint('  - $violation: ${violation.message}');
                    }
                  },
                  child: const Text('Check for violations'),
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

/// 1. ContrastRule — only the text color changes; background and size stay
/// fixed so contrast is the sole variable.
Widget _contrastDemo(bool fixed) {
  return Align(
    alignment: Alignment.centerLeft,
    child: Container(
      color: Colors.white,
      padding: const EdgeInsets.all(16),
      child: Text(
        fixed ? 'readable text' : 'low contrast text',
        style: TextStyle(
          color: fixed ? Colors.black87 : const Color(0xFFDDDDDD),
        ),
      ),
    ),
  );
}

/// 2. TapTargetSizeRule — always labeled, so only size varies.
Widget _tapTargetSizeDemo(bool fixed) {
  return Align(
    alignment: Alignment.centerLeft,
    child: Semantics(
      label: 'tap target',
      button: true,
      child: GestureDetector(
        onTap: () {},
        child: SizedBox(
          width: fixed ? 48 : 20,
          height: fixed ? 48 : 20,
          child: const ColoredBox(color: Colors.red),
        ),
      ),
    ),
  );
}

/// 3. SemanticLabelRule — always 48x48, so only the label varies.
Widget _semanticLabelDemo(bool fixed) {
  final target = GestureDetector(
    onTap: () {},
    child: const SizedBox(
      width: 48,
      height: 48,
      child: ColoredBox(color: Colors.deepPurple),
    ),
  );
  return Align(
    alignment: Alignment.centerLeft,
    child: fixed
        ? Semantics(label: 'favorite', button: true, child: target)
        // A semantics boundary, tightly wrapping just the target (not the
        // Align above, which would stretch this node's bounds to the
        // card's full width): without it, this unlabeled GestureDetector's
        // tap action merges upward into the card's title/description text
        // above, picking up their text as its own "label" and defeating
        // the demo — SemanticLabelRule then sees a non-empty label and
        // doesn't flag it.
        : Semantics(container: true, child: target),
  );
}

/// 4. FocusOrderRule — same two tap targets either way; only their build
/// order relative to their fixed left-to-right visual position changes.
Widget _focusOrderDemo(bool fixed) {
  return Align(
    alignment: Alignment.centerLeft,
    child: SizedBox(
      width: 116,
      height: 48,
      child: Stack(
        children: fixed
            ? const [
                Positioned(left: 0, child: _NamedTapTarget('A (built first)')),
                Positioned(
                  left: 68,
                  child: _NamedTapTarget('B (built second)'),
                ),
              ]
            : const [
                Positioned(left: 68, child: _NamedTapTarget('B (built first)')),
                Positioned(left: 0, child: _NamedTapTarget('A (built second)')),
              ],
      ),
    ),
  );
}

class _NamedTapTarget extends StatelessWidget {
  const _NamedTapTarget(this.label);

  final String label;

  @override
  Widget build(BuildContext context) {
    return Semantics(
      label: label,
      button: true,
      child: GestureDetector(
        onTap: () {},
        child: const SizedBox(
          width: 48,
          height: 48,
          child: ColoredBox(color: Colors.blue),
        ),
      ),
    );
  }
}

class _DemoCard extends StatelessWidget {
  const _DemoCard({
    required this.number,
    required this.rule,
    required this.description,
    required this.child,
  });

  final int number;
  final String rule;
  final String description;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '$number. $rule',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 4),
            Text(description, style: Theme.of(context).textTheme.bodySmall),
            const SizedBox(height: 12),
            child,
          ],
        ),
      ),
    );
  }
}
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