ethos 0.4.0 copy "ethos: ^0.4.0" to clipboard
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.

Pub License: Apache-2.0

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.X automatically from your ThemeData, and accepts explicit color_aliases for 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 1 on 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, InkResponse with tap-like gestures, plus any role: button alias from ethos.yaml.
  • Pass: wrapped in Semantics(label: '<non-empty literal>') as ancestor or descendant; or the alias label_arg is 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:

  1. Inline literals β€” TextStyle(color: Color(0xFF...), backgroundColor: ...).
  2. ThemeData extraction β€” resolves theme.textTheme.bodyLarge etc. automatically from your MaterialApp(theme: ThemeData(...)).
  3. color_aliases β€” resolves design-system expressions like $styles.text.body from your ethos.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 with size_guaranteed: true.
  • Verifiable: custom interactive widget inside a SizedBox or Container with literal width/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.); GestureDetector under a Focus, FocusScope, Shortcuts, or KeyboardListener ancestor; aliases with keyboard_ready: true.
  • Fail: GestureDetector.onTap with 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: Form widgets, or any layout with 2+ focusable inputs (TextField, Checkbox, Radio, etc.).
  • Pass: declares FocusNode, FocusScope, FocusTraversalGroup, or autofocus: 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 Semantics wrapper 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/$styles resolution for the contrast rule.
  • CI/CD integration examples (GitHub Actions, GitLab CI).

License #

Apache-2.0 β€” see LICENSE.

Author #

@gearscrafter β€” Mobile Developer.

Resources #


Made with ❀️ for inclusive Flutter apps.

3
likes
0
points
75
downloads

Publisher

verified publishergearscrafter.dev

Weekly Downloads

Measure accessibility coverage in Flutter apps using WCAG 2.2 specifications with Spec-Driven Development.

Repository (GitHub)
View/report issues

Topics

#accessibility #wcag #flutter #testing #cli

License

unknown (license)

Dependencies

analyzer, args, collection, path, yaml

More

Packages that depend on ethos