ethos 0.2.0
ethos: ^0.2.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 in five minutes. - โ 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.2.0
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 (CircleIconBtn, AppButton, whatever your team calls them). Ethos can't guess those names, so without config they appear as indeterminate. To fix that, drop an ethos.yaml next to your pubspec.yaml:
# ethos.yaml โ OPTIONAL. Ethos auto-detects it, no flag needed.
widget_aliases:
# Key = the widget's class name exactly as written in your 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
# Optional: tighten a threshold without rewriting the whole spec.
# rule_overrides:
# wcag_1_4_3_contrast_minimum:
# critical_threshold: 95
What each field teaches:
| 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. |
No ethos.yaml? Ethos still runs on the built-in spec. Custom widgets are simply ignored (shown 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(
label: 'Open profile',
child: GestureDetector(onTap: () {}, child: Icon(Icons.person)),
)
// โ
PASS โ descendant Semantics 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.
- In scope:
Textwith inlineTextStyle(color:, backgroundColor:)where both are literal (Color(0x...)orColors.*). - Indeterminate: colors from
Theme.of(context), custom style variables, or a missing inline background (the common case in well-structured Flutter code).
// โ
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))
// โ INDETERMINATE โ color from theme, cannot verify statically
Text('Hello', style: theme.textTheme.bodyLarge)
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.
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
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 # User ethos.yaml model + 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
โ โโโ 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 aliases
โ โโโ lib/ # Sample Dart files (analysis input)
โโโ test/
โ โโโ spec_compliance_test.dart
โ โโโ semantic_labels_detector_test.dart
โ โโโ contrast_detector_test.dart
โ โโโ touch_target_detector_test.dart
โ โโโ keyboard_detector_test.dart
โ โโโ focus_order_detector_test.dart
โโโ tool/
โโโ embed_spec.dart # Regenerates wcag_2_2_embedded.dart
If you edit the built-in spec (lib/src/specs/v1/wcag_2_2.yaml), regenerate the embedded constant:
dart run tool/embed_spec.dart
Roadmap #
v0.3.0 #
- Widget alias inheritance โ alias a widget once and have child widgets inherit its traits.
- Cross-method/cross-file resolution so a
Semanticswrapper in a parent widget is connected 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.