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: 44.6%
Compliance Level: NONE
๐ Coverage by Rule:
โ
Semantic Labels: 96% (26/27)
โน๏ธ Color Contrast: NO DATA โ 82 indeterminate
โน๏ธ Touch Targets: NO DATA โ 905 indeterminate
โ ๏ธ Keyboard Nav: 66% (8/12) โ CRITICAL
โ ๏ธ Focus Order: 0% (0/1) โ CRITICAL
โ ๏ธ Non-text Content: 15% (7/45) โ CRITICAL
โน๏ธ Resize Text: NO DATA (no hardcoded scale โ โ
correct)
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.
- โ
Watch mode (
ethos watch) โ re-analyzes only the changed file on every save, prints the full report withโฒ/โผdeltas, and highlights new findings. - โ
Flutter test integration โ
package:ethos/ethos_test.dartprovidesexpect(report, meetsAccessibilityLevel(WcagLevel.a))matchers andEthosTestHelper.analyzeSource(dartCode)for inline detector unit tests.
Installation
As a CLI
dart pub global activate ethos
# Verify installation
ethos --version
# From inside your Flutter project:
cd ./my_flutter_app
ethos
As a library
dependencies:
ethos: ^0.8.2
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
# Install locally as a global command
dart pub global activate --source path .
# Verify installation
ethos --version
# Analyze from inside a Flutter project โ no -p needed
cd ./my_flutter_app
ethos
ethos --deep -v
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 (from inside the project)
ethos
# Deep โ with live progress
ethos --deep -v
Deep mode falls back to standard automatically if the project context cannot
be built (e.g. flutter pub get has not been run).
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;
}
}
Getting started with ethos init
If your project uses a custom design system, the first thing to do after
installing Ethos is run ethos init. It scans your project for custom widgets
and unresolvable color expressions, then generates a starter ethos.yaml with
everything pre-filled โ you just review and fill in the values it can't infer.
# from inside your project
ethos init
Example output:
๐ Scanning ./my_app for custom widgets and color tokens...
Scanned 189 Dart files
Found 12 custom widget(s), 5 color expression(s)
โ
Generated: ./my_app/ethos.yaml
๐ฆ Custom widgets (fill in role and label_arg):
CircleIconBtn 47 uses
AppBtn 23 uses
WonderIllustration 18 uses
AppHeader 9 uses
๐จ Color expressions (add hex values to enable contrast checks):
$styles.text.body 31 uses
$styles.colors.offWhite 14 uses
Next steps:
1. Open ./my_app/ethos.yaml
2. Set role: for each widget_alias
3. Uncomment label_arg, size_guaranteed, keyboard_ready as needed
4. Fill in hex values under color_aliases
5. Run: ethos -v
The generated ethos.yaml looks like this โ nothing is invented, only
discovered. Values that require human knowledge are left as comments:
# ethos.yaml โ generated by `ethos init`
widget_aliases:
# CircleIconBtn โ used 47 times
CircleIconBtn:
role: button # button | text | input โ REQUIRED
# label_arg: ??? # e.g. semanticLabel, a11yLabel, label
# size_guaranteed: true
# keyboard_ready: true
# AppBtn โ used 23 times
AppBtn:
role: button
# label_arg: ???
color_aliases:
# used 31 times
# "$styles.text.body":
# foreground: "#REPLACE_ME"
# background: "#REPLACE_ME"
Once you fill in the values and re-run ethos -p ./my_app, the widgets that
were previously invisible (indeterminate) will start contributing to your
coverage score.
Live feedback with ethos watch
ethos watch performs an initial full scan and then re-analyzes only the
file you just saved. Every change prints the full report with coverage deltas
so you can see the impact of each edit immediately.
# from inside your project
ethos watch
# With deep analysis on each change:
ethos watch --deep
Example output after saving a file:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ buttons.dart changed (14:23:05)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
Initial scan complete (189 files)
๐ Overall: 71.4% โฒ +2.1% ยท Compliance: A
โ
Semantic Labels 85.0% (17/20) โฒ +5.0%
โน๏ธ Color Contrast 100.0% (8/8)
โ
Touch Targets 100.0% (8/8)
โ ๏ธ Keyboard Nav 66.7% (8/12) โผ -3.3%
โ
Focus Order 95.0% (19/20)
โ ๏ธ Non-text Content 15.6% (7/45) CRITICAL
โน๏ธ Resize Text NO DATA
๐ Findings in buttons.dart:
โ line 42 โ GestureDetector: Tap gesture has no keyboard alternative
Watching for changes... (Ctrl+C to stop)
Watch mode observes lib/, test/, and example/ with a 300 ms debounce.
Generated files (.g.dart, .freezed.dart) are ignored automatically.
Watch mode as a library
final engine = await WatchEngine.forProject('./my_app');
// Initial full scan
final baseline = await engine.initialScan(
onProgress: (current, total, path) {
print('[$current/$total] $path');
},
);
// Re-analyze on file change
final (newReport, diff) = await engine.reanalyzeFile(changedPath);
print('Overall delta: ${diff.overallDelta > 0 ? "โฒ" : "โผ"} ${diff.overallDelta.abs().toStringAsFixed(1)}%');
print('New critical rules: ${diff.newCritical}');
print('Resolved rules: ${diff.resolvedCritical}');
Testing your Flutter project's accessibility
Ethos ships a separate test utilities library so you can write accessibility
assertions directly inside your existing dart test or flutter_test suites.
Setup
Add Ethos as a dev dependency:
# pubspec.yaml
dev_dependencies:
ethos: ^0.8.2
test: ^1.24.0 # or flutter_test if you're in a Flutter project
Project-level accessibility test
Analyze your whole lib/ folder once in setUpAll and assert against the
report. This is the recommended pattern for CI gates:
// test/accessibility_test.dart
import 'package:test/test.dart';
import 'package:ethos/ethos_test.dart';
void main() {
group('Accessibility coverage', () {
late CoverageReport report;
setUpAll(() async {
report = await EthosTestHelper.analyzeProject('lib/');
});
test('meets WCAG Level A', () {
expect(report, meetsAccessibilityLevel(WcagLevel.a));
});
test('semantic labels above 80%', () {
expect(report, hasRuleCoverage(
'wcag_1_3_1_semantics_label',
greaterThan(80),
));
});
test('no critical failures', () {
expect(report, hasNoCriticalFailures());
});
test('no image accessibility issues', () {
expect(report, hasNoFindingsFor('wcag_1_1_1_non_text_content'));
});
});
}
Run it with:
dart test test/accessibility_test.dart
# or
flutter test test/accessibility_test.dart
Exit code 1 when any assertion fails โ plugs straight into CI.
Detector unit tests with inline source
EthosTestHelper.analyzeSource parses a Dart snippet in memory without
touching the filesystem. Use it to test specific widgets in isolation:
import 'package:test/test.dart';
import 'package:ethos/ethos_test.dart';
void main() {
group('Icon accessibility', () {
test('Icon with semanticLabel passes', () async {
final report = await EthosTestHelper.analyzeSource(
"Icon(Icons.search, semanticLabel: 'Search artifacts')",
);
expect(report, passesRule('wcag_1_1_1_non_text_content'));
});
test('Icon without semanticLabel fails', () async {
final report = await EthosTestHelper.analyzeSource('Icon(Icons.close)');
final cov = report.coverage['wcag_1_1_1_non_text_content'];
expect(cov?.findings, isNotEmpty);
});
});
group('GestureDetector accessibility', () {
test('with Semantics ancestor passes', () async {
final report = await EthosTestHelper.analyzeSource('''
Semantics(
label: 'Open profile',
child: GestureDetector(
onTap: () => navigate(),
child: Icon(Icons.person),
),
)
''');
expect(report, passesRule('wcag_1_3_1_semantics_label'));
});
test('without Semantics fails', () async {
final report = await EthosTestHelper.analyzeSource('''
GestureDetector(onTap: () => navigate(), child: Icon(Icons.settings))
''');
final cov = report.coverage['wcag_1_3_1_semantics_label'];
expect(cov?.findings, isNotEmpty);
});
});
group('Color contrast', () {
test('black on white passes', () async {
final report = await EthosTestHelper.analyzeSource(
"Text('Hello', style: TextStyle(color: Colors.black, backgroundColor: Colors.white))",
);
expect(report, passesRule('wcag_1_4_3_contrast_minimum'));
});
test('light grey on white fails', () async {
final report = await EthosTestHelper.analyzeSource(
"Text('Hello', style: TextStyle(color: Color(0xFFCCCCCC), backgroundColor: Color(0xFFFFFFFF)))",
);
final cov = report.coverage['wcag_1_4_3_contrast_minimum'];
expect(cov?.findings, isNotEmpty);
});
});
}
Available matchers
| Matcher | Description |
|---|---|
meetsAccessibilityLevel(WcagLevel.a) |
Overall coverage meets Level A (โฅ70%), AA (โฅ85%), or AAA (โฅ95%) |
passesRule('rule_id') |
A specific rule is not below its critical threshold |
hasRuleCoverage('rule_id', greaterThan(80)) |
Rule coverage satisfies any Matcher |
hasNoCriticalFailures() |
No rule is below its critical threshold |
hasNoFindingsFor('rule_id') |
No findings for a specific rule |
One-liner smoke test
For a quick CI check with no setup:
test('no critical a11y failures', () async {
await EthosTestHelper.expectNoCriticalFailures('lib/');
});
Throws a descriptive TestFailure listing every critical rule if any exist.
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
Seven 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.
6. Non-text Content โ wcag_1_1_1_non_text_content (WCAG 1.1.1 ยท Level A)
All images and icons that convey information must have a text alternative. Purely decorative content must be explicitly excluded from the semantic tree.
- In scope:
Image,Image.asset,Image.network,Image.file,SvgPicture.asset,SvgPicture.network,Icon. - Pass: wrapped in
Semantics(label: '...'), orexcludeFromSemantics: true(decorative); forIcon, a non-empty literalsemanticLabel:argument. - Indeterminate: label is a runtime variable.
- Fail: no label and no explicit decoration marker.
// โ
PASS โ informative image with label
Semantics(
label: 'Photo of the Colosseum',
child: Image.network(url),
)
// โ
PASS โ decorative image explicitly excluded
Image.asset('assets/bg.png', excludeFromSemantics: true)
// โ
PASS โ icon with semantic label
Icon(Icons.search, semanticLabel: 'Search artifacts')
// โ FAIL โ image without any accessibility annotation
Image.network(artifactUrl)
// โ FAIL โ icon without semanticLabel
Icon(Icons.close)
7. Resize Text โ wcag_1_4_4_resize_text (WCAG 1.4.4 ยท Level AA)
Text must be resizable up to 200% without loss of content. Hardcoding
textScaleFactor or textScaler to a fixed value ignores system font-size
preferences set by users with visual impairments.
- In scope: only
TextandMediaQuerywidgets that explicitly settextScaleFactorortextScaler. Widgets without these args are correct by default and are not counted. - Pass:
textScaleFactor: null(inherits system), or variable-based scaling that cannot be verified statically. - Fail: literal
textScaleFactor,TextScaler.noScaling, orTextScaler.linear(<literal>).
// โ
PASS โ inherits system preference (the right default)
Text('Hello')
// โ
PASS โ explicitly null (same as default)
Text('Hello', textScaleFactor: null)
// โ FAIL โ locks font size, ignores accessibility settings
Text('Hello', textScaleFactor: 1.0)
// โ FAIL โ forces no scaling
Text('Hello', textScaler: TextScaler.noScaling)
NO DATA is a good sign for Resize Text. It means your project does not override text scaling anywhere โ which is exactly what WCAG requires.
CLI reference
-p is optional in all commands โ when omitted, Ethos uses the current
working directory. Run cd my_flutter_app first and then:
ethos # standard analysis of current directory
ethos --deep -v # deep mode with progress
ethos init # generate ethos.yaml here
ethos watch # watch for changes here
ethos watch --deep # watch + deep
Or pass -p explicitly from anywhere:
ethos -p <project-path> [options]
ethos init -p <project-path>
ethos watch -p <project-path>
Options (analyze):
-p, --project-path Path to the Flutter project (default: current directory)
-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
Options (watch):
-p, --project-path Path to the Flutter project (default: current directory)
-d, --deep Use deep analysis on each change
-h, --help Show this help
More examples:
ethos -p ./my_app -r json -o report.json
ethos -p ./my_app -r markdown -o report.md
ethos init -p ./my_app -o path/to/ethos.yaml
Verbose logs go to stderr so ethos -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());
}
}
// Watch entry point
final engine = await WatchEngine.forProject('./my_app');
final baseline = await engine.initialScan();
final (newReport, diff) = await engine.reanalyzeFile(changedPath);
// Output
print(report.overallCoverage); // double 0โ100
print(report.complianceLevel); // 'A' | 'AA' | 'AAA' | 'NONE'
print(report.toJsonString()); // JSON for CI pipelines
Two separate import paths keep test utilities out of production builds:
import 'package:ethos/ethos.dart'; // main API โ use in production code
import 'package:ethos/ethos_test.dart'; // matchers + helpers โ use in tests only
Architecture
ethos/
โโโ bin/
โ โโโ analyze.dart # CLI (analyze + init + watch subcommands)
โโโ 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/ # 7 standard detectors
โ โ โโโ semantic_labels_detector.dart
โ โ โโโ contrast_detector.dart
โ โ โโโ touch_target_detector.dart
โ โ โโโ keyboard_detector.dart
โ โ โโโ focus_order_detector.dart
โ โ โโโ non_text_content_detector.dart
โ โ โโโ resize_text_detector.dart
โ โโโ 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
โ โโโ watch/
โ โ โโโ watch_engine.dart # Incremental cache + ReportDiff
โ โโโ init/
โ โโโ widget_discovery.dart # Scans for custom widgets/colors
โ โโโ ethos_yaml_generator.dart # Generates starter ethos.yaml
โโโ example/
โ โโโ main.dart
โ โโโ fixtures/
โ โโโ ethos.yaml
โ โโโ lib/
โโโ tool/
โโโ embed_spec.dart
Roadmap
v1.0.0
- Animation preferences detector (
wcag_2_3_1) โ flagAnimationControllerand transitions that don't respectMediaQuery.disableAnimations. - Configurable rule subset โ run only the rules you care about.
- Stable public API with full WCAG 2.2 Level AA coverage.
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.
Libraries
- ethos
- Ethos โ Measure accessibility coverage in Flutter apps using WCAG 2.2 specifications with Spec-Driven Development.
- ethos_test
- Test utilities for Ethos โ matchers and helpers for use in
dart testorflutter_testsuites.