ethos 0.5.1
ethos: ^0.5.1 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. - โ
Deep analysis mode (
--deep) โ usesAnalysisContextCollectionto resolve cross-file references, class hierarchies, and type information. EmitsStream<AnalysisProgress>events for live progress. Falls back to standard mode automatically if the project is not ready. - โ Pluggable detector registry โ add or replace rules without touching the core engine.
Installation #
As a CLI #
dart pub global activate ethos
ethos -p ./my_flutter_app
As a library #
dependencies:
ethos: ^0.4.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
Standard vs Deep analysis #
Ethos has two analysis modes:
| Standard | Deep (--deep) |
|
|---|---|---|
| Speed | Fast (seconds) | Slower (10โ60s) |
| Cross-file resolution | โ | โ |
| Class hierarchy traversal | โ | โ |
| Variable type resolution | โ | โ |
| Progress stream | โ | โ |
Requires flutter pub get |
โ | โ (auto-detects) |
Standard mode is ideal for quick checks and CI gates. Deep mode is for comprehensive audits โ it finds widgets that standard mode misses because they are defined in a different file from where they are used.
# Standard
ethos -p ./my_app
# Deep โ with live progress
ethos -p ./my_app --deep -v
Deep mode falls back to standard automatically if .dart_tool/package_config.json
is missing.
Deep mode as a library #
final deepAnalyzer = await DeepAnalyzer.forProject('./my_app');
await for (final event in deepAnalyzer.analyze()) {
switch (event) {
case AnalysisLoadingContext(:final totalFiles):
print('Loading $totalFiles files...');
case AnalysisAnalyzingFile(:final current, :final total):
print('[$current/$total]');
case AnalysisWarning(:final message):
print('โ ๏ธ $message');
case AnalysisComplete():
final report = (event as AnalysisComplete).report;
print(report.toJsonString());
default:
break;
}
}
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".
"$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
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, the element remains indeterminate.
No ethos.yaml? Ethos still runs on the built-in spec. For a vanilla Flutter
project that's already useful; for a project with a design system, the aliases
make all the difference โ and deep mode can discover many of them automatically.
Supported rules #
Five built-in rules, all backed by RecursiveAstVisitor on real Dart AST.
Deep mode runs enhanced versions of rules 1 and 2.
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. - Deep mode: also follows widget definitions across files and detects
cross-method
Semanticswrappers.
// โ
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). Resolution is attempted in layers:
- Inline literals โ
TextStyle(color: Color(0xFF...), backgroundColor: ...). - ThemeData extraction โ resolves
theme.textTheme.bodyLargeetc. color_aliasesโ resolves design-system expressions fromethos.yaml.- Deep mode only โ follows variable references across files.
// โ
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 in standard mode; resolved in deep mode
Text('Hello', style: TextStyle(color: bodyColor)) // bodyColor defined elsewhere
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; aliases withsize_guaranteed: true. - Verifiable: custom widget in a
SizedBox/Containerwith literal dimensions. - Indeterminate: size from a variable or intrinsic content.
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;
GestureDetectorunderFocus/FocusScope/Shortcuts/KeyboardListener; aliases withkeyboard_ready: true. - Fail:
GestureDetector.onTapwith no keyboard path. - Excluded:
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 layouts with 2+ focusable inputs. - 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
-d, --deep Deep analysis: resolves types and cross-file references.
Slower but more precise. Requires `flutter pub get`.
Falls back to standard mode if project is not ready.
-v, --verbose Show progress details (written to stderr)
-h, --help Show this help
Examples:
ethos -p ./my_app
ethos -p ./my_app --deep
ethos -p ./my_app --deep -v
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
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');
final report = await analyzer.analyze();
// Deep entry point
final deepAnalyzer = await DeepAnalyzer.forProject('./my_app');
await for (final event in deepAnalyzer.analyze()) {
if (event is AnalysisComplete) {
final report = (event as AnalysisComplete).report;
print(report.toJsonString());
}
}
// Output
print(report.overallCoverage); // double 0โ100
print(report.complianceLevel); // 'A' | 'AA' | 'AAA' | 'NONE'
print(report.toJsonString()); // JSON for CI pipelines
Architecture #
ethos/
โโโ bin/
โ โโโ analyze.dart # CLI entry point (--deep flag)
โโโ 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 # Standard engine
โ โโโ spec_loader.dart
โ โโโ detector_registry.dart
โ โโโ rule_detector.dart # RuleDetector interface
โ โโโ ast/widget_visitor.dart
โ โโโ utils/
โ โ โโโ color_resolver.dart
โ โ โโโ theme_extractor.dart
โ โโโ detectors/ # 5 standard detectors
โ โโโ deep/
โ โโโ deep_analyzer.dart # Deep engine (Stream)
โ โโโ deep_detector.dart # DeepDetector interface
โ โโโ analysis_progress.dart # Sealed class: 6 event types
โ โโโ resolved_file.dart # ResolvedFile + ProjectIndex
โ โโโ detectors/
โ โโโ cross_file_semantic_labels_detector.dart
โ โโโ resolved_contrast_detector.dart
โโโ example/
โ โโโ main.dart
โ โโโ fixtures/
โ โโโ ethos.yaml
โ โโโ lib/
โโโ 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.