ethos 0.4.0
ethos: ^0.4.0 copied to clipboard
Measure accessibility coverage in Flutter apps using WCAG 2.2 specifications with Spec-Driven Development.
Ethos #
Measure accessibility coverage in Flutter apps using WCAG 2.2 specifications with Spec-Driven Development.
What is Ethos? #
Ethos measures what percentage of your Flutter widgets comply with WCAG 2.2 accessibility standards.
Unlike tools that detect individual issues, Ethos calculates coverage metrics for each rule, giving you a clear picture of your app's overall accessibility maturity.
๐ Overall Coverage: 75.5%
โ
Compliance Level: AA
๐ Coverage by Rule:
โ
Semantic Labels: 85% (17/20)
โ ๏ธ Color Contrast: 60% (6/10) โ CRITICAL
โ
Touch Targets: 100% (12/12)
โ
Keyboard Nav: 90% (9/10)
โ
Focus Order: 95% (19/20)
โ 6 indeterminate (color from theme โ not counted)
Features #
- โ WCAG 2.2 alignment โ coverage metrics, not one-off issue lists.
- โ
Honest AST analysis with
package:analyzerโ no regex, no guessing. - โ Indeterminate accounting โ values from themes or runtime variables are reported separately and never inflate pass/fail ratios.
- โ Built-in spec, zero setup โ the WCAG 2.2 rules ship inside the package. You don't copy any YAML file.
- โ
Optional
ethos.yamlโ teach Ethos about your own design-system widgets and colors in five minutes. - โ
Theme-aware contrast โ resolves
theme.textTheme.Xautomatically from yourThemeData, and accepts explicitcolor_aliasesfor custom style variables. - โ Pluggable detector registry โ add or replace rules without touching the core engine.
- โ
CI/CD ready โ JSON, Markdown, and human-readable outputs; exits with
code
1on critical failures.
Installation #
As a CLI #
dart pub global activate ethos
ethos -p ./my_flutter_app
As a library #
dependencies:
ethos: ^0.3.1
import 'package:ethos/ethos.dart';
void main() async {
final analyzer = await CoverageAnalyzer.forProject('./my_flutter_app');
final report = await analyzer.analyze();
print('Coverage: ${report.overallCoverage}%');
print('Compliance: ${report.complianceLevel}');
}
You do not copy any spec file. The built-in WCAG 2.2 spec lives inside the package.
Quick start (local development) #
git clone https://github.com/gearscrafter/ethos.git
cd ethos
dart pub get
# Run against the bundled fixtures
dart run example/main.dart
# Run against your own Flutter project
dart run bin/analyze.dart -p ./my_flutter_app
# Install locally as a global command
dart pub global activate --source path .
ethos -p ./my_flutter_app
Configuration (optional): ethos.yaml #
Ethos works out of the box โ the built-in spec already covers Flutter's
standard widgets (GestureDetector, InkWell, IconButton, TextField,
etc.).
Most real apps wrap controls in their own design-system components and define
colors in a custom style object. Drop an ethos.yaml next to your
pubspec.yaml to teach Ethos about them:
# ethos.yaml โ OPTIONAL. Ethos auto-detects it; no flag needed.
widget_aliases:
# Key = your widget's class name exactly as written in code.
CircleIconBtn:
role: button # button | text | input
label_arg: semanticLabel # which arg carries the accessible label
size_guaranteed: true # already wraps a >= 48ร48 target internally?
keyboard_ready: true # keyboard-operable out of the box?
AppButton:
role: button
label_arg: a11yLabel
color_aliases:
# Teach Ethos about your design-system color expressions so the contrast
# rule can compute real WCAG ratios instead of reporting "indeterminate".
# Key = exact source expression as written in code.
"$styles.text.body":
foreground: "#212121" # required โ the text color
background: "#FFFFFF" # optional โ the default background color
"$styles.colors.primary":
foreground: "#1565C0"
# Optional: tighten a threshold without rewriting the spec.
# rule_overrides:
# wcag_1_4_3_contrast_minimum:
# critical_threshold: 95
What each section teaches:
widget_aliases
| Field | Detector | Effect |
|---|---|---|
role: button |
Semantic Labels, Keyboard, Touch Target | Widget counts as an interactive control. |
label_arg |
Semantic Labels | Look for the semantic label in this argument. |
size_guaranteed |
Touch Target Size | Auto-PASS โ already โฅ 48ร48 internally. |
keyboard_ready |
Keyboard Accessibility | Auto-PASS โ keyboard-operable out of the box. |
color_aliases
Maps a design-system style expression to concrete hex colors. Both
#RRGGBB and #AARRGGBB formats are accepted. When background is
omitted, Ethos cannot compute a ratio and the element remains indeterminate.
No ethos.yaml? Ethos still runs on the built-in spec. Custom widgets and
colors appear as indeterminate. For a vanilla Flutter project that's already
useful; for a project with a design system, the aliases make all the
difference.
Supported rules #
Five built-in rules, all backed by a RecursiveAstVisitor on real Dart AST.
1. Semantic Labels โ wcag_1_3_1_semantics_label (WCAG 1.3.1 ยท Level A) #
Custom interactive widgets must have an accessible label.
- In scope:
GestureDetector,InkWell,InkResponsewith tap-like gestures, plus anyrole: buttonalias fromethos.yaml. - Pass: wrapped in
Semantics(label: '<non-empty literal>')as ancestor or descendant; or the aliaslabel_argis a non-empty literal. - Indeterminate: label is a variable, interpolation, or runtime call.
- Excluded automatically:
excludeFromSemantics: true, drag/pan-only gestures,onTap: () {}(block-parent), tap-to-dismiss patterns.
// โ
PASS โ Semantics as ancestor
Semantics(
label: 'Open profile',
child: GestureDetector(onTap: () {}, child: Icon(Icons.person)),
)
// โ
PASS โ Semantics as descendant also works
GestureDetector(
onTap: () => navigate(),
child: Semantics(label: 'Go to settings', child: Icon(Icons.settings)),
)
// โ FAIL
GestureDetector(onTap: () => navigate(), child: Icon(Icons.settings))
2. Minimum Color Contrast โ wcag_1_4_3_contrast_minimum (WCAG 1.4.3 ยท Level AA) #
Text must have at least 4.5:1 contrast (3:1 for large text โฅ 18 pt) using the real WCAG luminance formula. Resolution is attempted in three layers:
- Inline literals โ
TextStyle(color: Color(0xFF...), backgroundColor: ...). - ThemeData extraction โ resolves
theme.textTheme.bodyLargeetc. automatically from yourMaterialApp(theme: ThemeData(...)). color_aliasesโ resolves design-system expressions like$styles.text.bodyfrom yourethos.yaml.
// โ
PASS โ ratio 21:1
Text('Hello', style: TextStyle(
color: Colors.black,
backgroundColor: Colors.white,
))
// โ FAIL โ ratio ~1.6:1
Text('Hello', style: TextStyle(
color: Color(0xFFCCCCCC),
backgroundColor: Colors.white,
))
Text('Hello', style: theme.textTheme.bodyLarge)
// โ
PASS via color_aliases (if declared in ethos.yaml)
// Text('Hello', style: $styles.text.body)
3. Touch Target Size โ wcag_2_5_5_target_size_enhanced (WCAG 2.5.5 ยท Level AAA) #
Interactive elements must be at least 48ร48 logical pixels.
- Auto-pass:
IconButton,FloatingActionButton(Flutter guarantees 48ร48); aliases withsize_guaranteed: true. - Verifiable: custom interactive widget inside a
SizedBoxorContainerwith literalwidth/heightโ pass if both โฅ 48, fail otherwise. - Indeterminate: size from a variable, intrinsic content, or
tapTargetSize: shrinkWrap.
4. Keyboard Accessibility โ wcag_2_1_1_keyboard (WCAG 2.1.1 ยท Level A) #
All interactive functionality must be reachable by keyboard.
- Pass: Material controls (
ElevatedButton,TextField,InkWell, etc.);GestureDetectorunder aFocus,FocusScope,Shortcuts, orKeyboardListenerancestor; aliases withkeyboard_ready: true. - Fail:
GestureDetector.onTapwith no keyboard path in its ancestor chain. - Excluded: widgets with
excludeFromSemantics: true(visual-only wrappers).
5. Focus Order โ wcag_2_4_3_focus_order (WCAG 2.4.3 ยท Level A) #
Multi-input layouts must declare explicit focus management.
- In scope:
Formwidgets, or any layout with 2+ focusable inputs (TextField,Checkbox,Radio, etc.). - Pass: declares
FocusNode,FocusScope,FocusTraversalGroup, orautofocus: true.
CLI reference #
ethos -p <project-path> [options]
Options:
-p, --project-path Path to the Flutter project to analyze (required)
-c, --config Path to a custom ethos.yaml (default: auto-detect)
-r, --report-type Output format: human | json | markdown | coverage
(default: human)
-o, --output Write report to this file instead of stdout
-v, --verbose Show progress details (written to stderr)
-h, --help Show this help
Examples:
ethos -p ./my_app
ethos -p ./my_app -c path/to/ethos.yaml
ethos -p ./my_app -r json -o report.json
ethos -p ./my_app -r markdown -o report.md
ethos -p ./my_app -v
Verbose logs go to stderr so ethos -p . -r json | jq works cleanly.
Exit code 1 when any rule is below its critical threshold โ useful as a CI
gate.
Compliance levels #
| Level | Minimum coverage | Description |
|---|---|---|
| AAA | โฅ 95% | Enhanced accessibility |
| AA | โฅ 85% | Strong accessibility (typical target) |
| A | โฅ 70% | Basic accessibility |
| NONE | < 70% | Does not meet minimum standards |
Library API reference #
// Standard entry point โ built-in spec + optional ethos.yaml auto-merge
final analyzer = await CoverageAnalyzer.forProject('./my_app');
// With explicit config file
final analyzer = await CoverageAnalyzer.forProject(
'./my_app',
configPath: 'path/to/ethos.yaml',
);
// Run analysis
final report = await analyzer.analyze();
// Output options
print(report.overallCoverage); // double 0โ100
print(report.complianceLevel); // 'A' | 'AA' | 'AAA' | 'NONE'
print(report.toJsonString()); // JSON for CI pipelines
Advanced: CoverageAnalyzer.loadFromFile(specPath, projectPath: ...) and
.fromString(yaml, projectPath: ...) load a fully custom spec instead of the
built-in.
Architecture #
ethos/
โโโ bin/
โ โโโ analyze.dart # CLI entry point
โโโ lib/
โ โโโ ethos.dart # Public barrel export
โ โโโ src/
โ โโโ models/
โ โ โโโ spec.dart # Spec, Rule, WidgetAlias, WidgetRole
โ โ โโโ ethos_config.dart # EthosConfig, ColorAlias, RuleOverride
โ โ โโโ coverage_report.dart # CoverageReport, RuleCoverage, Finding
โ โโโ specs/v1/
โ โ โโโ wcag_2_2.yaml # Source spec โ edit this
โ โ โโโ wcag_2_2_embedded.dart # Generated constant โ do not edit
โ โโโ analyzer/
โ โโโ coverage_analyzer.dart # Engine (forProject / analyze)
โ โโโ spec_loader.dart # Built-in + ethos.yaml merge
โ โโโ detector_registry.dart
โ โโโ rule_detector.dart # RuleDetector interface
โ โโโ ast/widget_visitor.dart
โ โโโ utils/
โ โ โโโ color_resolver.dart # WCAG luminance + Colors.* map
โ โ โโโ theme_extractor.dart # ThemeData color extraction
โ โโโ detectors/
โ โโโ semantic_labels_detector.dart
โ โโโ contrast_detector.dart
โ โโโ touch_target_detector.dart
โ โโโ keyboard_detector.dart
โ โโโ focus_order_detector.dart
โโโ example/
โ โโโ main.dart
โ โโโ fixtures/
โ โโโ ethos.yaml # Sample widget + color aliases
โ โโโ lib/ # Sample Dart files (analysis input only)
โโโ test/
โโโ tool/
โโโ embed_spec.dart # Regenerates wcag_2_2_embedded.dart
When editing the built-in spec (lib/src/specs/v1/wcag_2_2.yaml), regenerate
the embedded constant:
dart run tool/embed_spec.dart
Roadmap #
v1.0.0 #
- Widget alias inheritance โ alias a widget once and child widgets inherit its traits automatically.
- Cross-method/cross-file resolution so a
Semanticswrapper in a parent widget connects to a custom button in a child. - More built-in detectors: text scaling, alternative text on images, animation preferences.
- Configurable rule subset โ run only the rules you care about.
Contributing #
Contributions are welcome. High-value areas:
- Additional WCAG 2.2 detectors.
- Improved theme/
$stylesresolution for the contrast rule. - CI/CD integration examples (GitHub Actions, GitLab CI).
License #
Apache-2.0 โ see LICENSE.
Author #
@gearscrafter โ Mobile Developer.
Resources #
- WCAG 2.2 Quick Reference
- Flutter Accessibility Docs
- Material Design 3 โ Accessibility
- WebAIM Contrast Checker
Made with โค๏ธ for inclusive Flutter apps.