This package provides a set of custom lint rules to help catch common accessibility issues in Flutter code. Rules run via the analysis_server_plugin integration for IDE support, and via a standalone CLI tool (a11y_analyze) for use in CI pipelines.
In your project's pubspec.yaml:
dependencies:
a11y_linter:
git:
url: https://github.com/baseflow/accessibility_linter.gitplugins:
a11y_linter:
path: <path-to-package> # or use pub cache path resolved by pub get
diagnostics:
orientation_lock: true
missing_persistent_input_label: true
missing_focus_indicator: true
missing_semantics_label: true
insufficient_color_contrast: true
insufficient_tap_target_size: trueThis enables IDE integration in VS Code and IntelliJ/Android Studio via the Dart analysis server.
Add the following steps to your GitHub Actions workflow (or equivalent):
- name: Install deps
run: flutter pub get
- name: Generate code
run: dart run build_runner build
- name: Run a11y linter
run: dart run a11y_linter:a11y_analyze lib/The a11y_analyze script exits with code 1 if any violations are found, making it suitable as a CI gate. It does not depend on the analysis server plugin infrastructure and works reliably in headless environments.
To run the linter from your local machine or CI environment, use:
dart run a11y_linter:a11y_analyze lib/Or for a Dart project (not Flutter):
dart run a11y_linter:a11y_analyze lib/You can also specify a custom path:
dart run a11y_linter:a11y_analyze src/The script will analyze all .dart files in the specified directory (default: lib/) and report any violations found. Output includes the file path, line number, column number, rule name, and message for each violation.
Ensures Icon, Image, and ImageIcon provide an accessible label via semanticLabel, are wrapped with Semantics(label: ...), or are marked decorative with ExcludeSemantics. Also checks clickable widgets with icon-only content (e.g. IconButton) for a tooltip or an accessible icon label.
See Non-textual elements and Decorative elements of the Baseflow Accessibility Guidelines.
Flags interactive widgets that do not expose a visible focus indicator. Suggests providing focusColor or a FocusNode, or wrapping with ExcludeSemantics if intentionally non-interactive.
See Focus indication of the Baseflow Accessibility Guidelines.
Ensures input widgets (TextField, TextFormField) expose a persistent label via InputDecoration(labelText: ...) or InputDecoration(label: ...) rather than relying solely on placeholder/hint text.
See Form validation & labels of the Baseflow Accessibility Guidelines.
Detects calls that lock device orientation via SystemChrome.setPreferredOrientations and warns that orientation locking should be avoided for accessibility.
See Screen orientation of the Baseflow Accessibility Guidelines.
Statically checks color literals for text, icons, and other foreground elements against nearby background colors and reports when contrast falls below WCAG AA thresholds (4.5:1 for normal text, 3:1 for large text). Only statically determinable colors (literals, Colors.*, Color(0x...), etc.) are checked; theme colors and runtime expressions are skipped.
See Color contrast of the Baseflow Accessibility Guidelines.
Warns when tappable widgets (GestureDetector, InkWell, buttons, etc.) are constrained below 24×24 logical pixels, per WCAG 2.5.8.
Each rule lives entirely in a single self-contained file under lib/src/rules/. Adding a new rule requires touching exactly two files:
Create a class extending A11yRule. Override name, message, correctionMessage, and whichever check method(s) apply:
import 'package:analyzer/dart/ast/ast.dart';
import '../shared/a11y_rule.dart';
class MyNewRule extends A11yRule {
@override
String get name => 'my_new_rule';
@override
String get message => 'Short description of what is wrong.';
@override
String get correctionMessage => 'Suggestion shown in the IDE on how to fix it.';
@override
void checkInstanceCreation(
InstanceCreationExpression node,
void Function(AstNode) report,
) {
// Inspect the AST node. Call report(node) when a violation is detected.
}
}Override checkInstanceCreation for widget constructor calls, checkMethodInvocation for static/instance method calls, or both if needed. Unneeded methods can be omitted — the default implementations are no-ops.
Add one import and one instance to the list:
import '../rules/my_new_rule.dart';
final List<A11yRule> allRules = [
// ... existing rules ...
MyNewRule(),
];That's it. Both the IDE plugin and the CLI a11y_analyze tool pick it up automatically — no other changes needed.
plugins:
a11y_linter:
diagnostics:
my_new_rule: truelib/
main.dart # IDE plugin entry point — loops over allRules
src/
rules/
a11y_analysis_rule.dart # Generic AnalysisRule wrapper (IDE only)
orientation_lock.dart # A11yRule subclass (one file per rule)
missing_semantics_label.dart
missing_focus_indicator.dart
missing_persistent_input_label.dart
insufficient_tap_target_size.dart
insufficient_color_contrast.dart
shared/
a11y_rule.dart # Abstract base class for all rules
all_rules.dart # Canonical list — the only registration point
color_data.dart # Known Flutter color values
cli/
checker.dart # RecursiveAstVisitor driven by allRules
runner.dart # AnalysisContextCollection setup
violation.dart # Violation data class
utils/
ast_utils.dart # Shared AST helpers
bin/
a11y_analyze.dart # CLI entry point
A11yRule is the bridge between both entry points. The CLI drives a RecursiveAstVisitor from allRules, calling checkInstanceCreation and checkMethodInvocation on each rule directly. The IDE wraps each rule in a generic A11yAnalysisRule that registers SimpleAstVisitor callbacks with the analysis server registry. The default no-op implementations on A11yRule mean rules only override the methods they need.