references/audit-templates.md
# Accessibility — Audit Report Templates
Audit report templates for WCAG 2.2 conformance levels (A, AA, AA + selected AAA, AAA), with severity guide, the report template, and per-level passed-check lists. Findings reference WCAG 2.2 criterion IDs and call out the version (2.0, 2.1, or 2.2) so readers can trace the source.
---
## Severity Guide
| Severity | Meaning |
| --- | --- |
| **CRITICAL** | Blocks assistive technology users entirely. Fix before merging. |
| **MAJOR** | Significant barrier. Fix in current sprint. |
| **MINOR** | Degraded experience or polish item. Schedule for next sprint. |
Severity assignment:
- **CRITICAL**. Criterion applies at the selected level AND the issue completely blocks the use case (no semantic label on a primary action, `GestureDetector` on a required flow, focused field obscured by a sticky bar, target below 24 dp, drag-only operation with no alternative, puzzle CAPTCHA with no alternative).
- **MAJOR**. Criterion applies at the selected level AND the issue significantly degrades the experience (contrast fails by more than one point, target between 24 and 48 dp at AA, dialog does not trap focus, `Dismissible` only exposes a button after a swipe).
- **MINOR**. Criterion applies AND the issue is a refinement (contrast fails marginally, live region missing on non-critical status, focus border 1 px instead of 2 px, redundant entry on a single optional field).
---
## Cross-Platform Severity Adjustments
The same code path can produce different severities per platform. Use this table to decide how to assign severity per platform when listing a finding.
| Issue | iOS | Android | Web | macOS | Win/Linux |
| --- | --- | --- | --- | --- | --- |
| `GestureDetector` for tap | CRITICAL | CRITICAL | CRITICAL | CRITICAL | CRITICAL |
| 16x16 target (below 24 dp) | CRITICAL | CRITICAL | CRITICAL | MAJOR | MAJOR |
| 36 dp target (between 24 and 48 dp) | MAJOR | MAJOR | MAJOR | MINOR | MINOR |
| `Dismissible` without delete button | CRITICAL | CRITICAL | MAJOR | MAJOR | MAJOR |
| Focused field obscured by sticky bottom bar | CRITICAL | CRITICAL | MAJOR | MAJOR | MAJOR |
| `AnimatedContainer` ignoring disableAnimations | MAJOR | MAJOR | MAJOR | MAJOR | MAJOR |
| Tooltip > 80 chars | MINOR | MINOR | MAJOR | MINOR | MINOR |
| `setApplicationSwitcherDescription` for page title | n/a | n/a | CRITICAL | n/a | n/a |
| Bypass blocks missing | n/a | n/a | MAJOR | n/a | n/a |
| Cupertino widget without semantic wrapper | MAJOR | MINOR | MINOR | MAJOR | MINOR |
| Hardcoded Color outside ThemeExtension | MINOR | MINOR | MINOR | MAJOR | MAJOR (Windows HCM) |
Severities in this table assume the criterion is active at the selected level. Findings for criteria above the selected level (for example, AAA criteria when AA is selected) are dropped from the report unless the criterion is in the user's "selected AAA" list from Phase 1.
---
## Report Template (all levels)
```markdown
# Flutter Accessibility Audit
**Date:** YYYY-MM-DD
**WCAG Level:** [A | AA | AA + selected AAA | AAA]
**Selected AAA criteria (if applicable):** [e.g., 1.4.6, 2.2.3, 2.4.13]
**Platforms:** [iOS | Android | Web | macOS | Windows/Linux | combination]
**Files audited:**
- path/to/file.dart
## Summary
| Severity | Count |
|----------|-------|
| CRITICAL | 0 |
| MAJOR | 0 |
| MINOR | 0 |
## Findings
### 1. [Short descriptive title]
- **File:** path/to/file.dart ~L42
- **WCAG:** [criterion ID] [criterion name] (Level [A/AA/AAA], WCAG [2.0/2.1/2.2])
- **Platform(s):** [iOS | Android | Web | macOS | Windows/Linux | All]
- **Severity:** [CRITICAL | MAJOR | MINOR]
- **Issue:** [description]
- **Fix:**
// Before
[existing code]
// After
[fixed code]
### 2. [Next finding...]
## Passed Checks
[copy the applicable checks from the level lists below]
## Out of Scope
[criteria the team explicitly opted out of, with rationale]
```
---
## Passed Checks — Level A
```text
- [x] A · Semantics & Screen Reader — images/icons have semantic labels; roles correct (1.1.1, 1.3.1, 4.1.2)
- [x] B · Touch Targets — interactive elements present (size assessed at AA)
- [x] C · Focus & Keyboard — all interactions keyboard-reachable; no traps (2.1.1, 2.1.2)
- [x] D · Color — color is never sole differentiator (1.4.1)
- [x] E · Text Scaling — no fixed-height text containers
- [x] F · Animation — no content flashes > 3 Hz (2.3.1)
- [x] G · Forms & Help — form errors identified in text (3.3.1); labels present (3.3.2)
- [x] H · Redundant Entry — multi-step forms reuse session data (3.3.7, WCAG 2.2)
- [x] I · Consistent Help — help mechanism placed consistently across screens (3.2.6, WCAG 2.2)
- [x] J · Status Messages — async updates announced via liveRegion or SemanticsService (4.1.3)
- [x] K · Web only — bypass blocks (2.4.1) and page titled (2.4.2) in place
```
## Passed Checks — Level AA (Level A plus these)
```text
- [x] B · Target Size Minimum — all targets >= 24x24 CSS px (2.5.8, WCAG 2.2)
- [x] B · Dragging Alternatives — every drag operation has a single-pointer alternative (2.5.7, WCAG 2.2)
- [x] C · Focus Visible — keyboard focus indicator always visible (2.4.7)
- [x] C · Focus Not Obscured (Min) — focused widget never entirely hidden by sticky/overlay content (2.4.11, WCAG 2.2)
- [x] D · Color Contrast — normal text >= 4.5:1, large text >= 3:1, UI components >= 3:1 (1.4.3, 1.4.11)
- [x] E · Text Scaling — text scales to 200% (300% on iOS) without loss (1.4.4)
- [x] F · Animation — every animation gated on disableAnimations (2.3.3 carryover)
- [x] G · Orientation — not locked to single orientation (1.3.4)
- [x] G · Input Purpose — autofillHints and keyboardType correct on personal-data fields (1.3.5)
- [x] G · Accessible Authentication (Min) — paste allowed, password managers supported, no required cognitive test without alternative (3.3.8, WCAG 2.2)
- [x] H · Reflow — content reflows at 320 CSS px equivalent (1.4.10) [Web/desktop]
- [x] H · Hover/Focus content — dismissable, hoverable, persistent (1.4.13) [Web/desktop]
```
## Passed Checks — AA + selected AAA
Use the AA list above, then append a row for each selected AAA criterion. Common opt-ins:
```text
- [x] D · Contrast Enhanced — normal text >= 7:1, large text >= 4.5:1 (1.4.6) [scoped to: <flow names>]
- [x] F · Animation from Interactions — zero motion when disableAnimations is true (2.3.3) [scoped to: <flow names>]
- [x] C · Focus Appearance — focus indicator >= 2 CSS px perimeter, 3:1 vs unfocused (2.4.13, WCAG 2.2) [scoped to: <flow names>]
- [x] J · No Timing — no time limits except for real-time events (2.2.3) [scoped to: <flow names>]
```
## Passed Checks — Level AAA (Level AA plus these)
```text
- [x] B · Target Size Enhanced — interactive elements >= 44x44 CSS px (2.5.5)
- [x] C · Keyboard (No Exception) — no GestureDetector anywhere (2.1.3)
- [x] C · Focus Not Obscured (Enhanced) — no occlusion at all (2.4.12, WCAG 2.2)
- [x] C · Focus Appearance — >= 2 CSS px perimeter, 3:1 vs unfocused (2.4.13, WCAG 2.2)
- [x] D · Contrast Enhanced — 7:1 / 4.5:1 (1.4.6)
- [x] F · Animation — zero flashing content (2.3.2)
- [x] J · No Timing — no mandatory time limits (2.2.3)
- [x] J · Timeouts — inactivity warning present (2.2.6)
- [x] K · Location — breadcrumbs or current-screen indication (2.4.8)
- [x] K · Concurrent Input — no single-modality restriction (2.5.6)
- [x] G · Accessible Authentication (Enhanced) — no required cognitive test even with alternative (3.3.9, WCAG 2.2)
- [x] G · Help — context-sensitive help available (3.3.5)
- [x] G · Error Prevention (All) — all submissions reversible or confirmable (3.3.6)
```
---
## Per-Platform Annotations on Findings
When multiple platforms are selected, every finding's Platform(s) row should be a comma-separated list. The same code path can produce different severities per platform. Example:
```markdown
### 3. SnackBar overlays focused TextField at the bottom of the form
- **File:** lib/checkout/checkout_view.dart ~L88
- **WCAG:** 2.4.11 Focus Not Obscured (Minimum) (Level AA, WCAG 2.2)
- **Platform(s):** iOS (CRITICAL), Android (CRITICAL), Web (MAJOR), macOS (MAJOR), Windows/Linux (MAJOR)
- **Severity:** CRITICAL (mobile), MAJOR (desktop, web)
- **Issue:** When the keyboard opens on iOS or Android, the SnackBar pushes up and entirely covers the focused TextField. On desktop and web the TextField stays partially visible but the SnackBar still occludes the bottom edge.
- **Fix:** [diff]
```
If a finding only applies to a subset of selected platforms, list only those platforms and add a note in the Issue field explaining which platforms were checked but not affected.
references/examples.md
# Accessibility — Extended Examples
Detailed Flutter code examples for every category in the skill: semantics and screen reader, touch targets and dragging alternatives, focus and keyboard navigation (including the new WCAG 2.2 focus-not-obscured rules), color contrast, text scaling, animation and motion, and forms / authentication / help.
The examples in this file cover every WCAG 2.2 criterion the skill checks. Read this file when applying a fix during Phase 4.
---
## Semantics & Screen Reader — Extended Examples
### Custom Semantics for Complex Widgets
```dart
import 'package:flutter/material.dart';
/// A rating bar that provides a single semantic description
/// instead of exposing individual star icons.
class AccessibleRatingBar extends StatelessWidget {
const AccessibleRatingBar({
required this.rating,
required this.maxRating,
super.key,
});
final int rating;
final int maxRating;
@override
Widget build(BuildContext context) {
return Semantics(
label: 'Rating: $rating out of $maxRating stars',
child: ExcludeSemantics(
child: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(maxRating, (index) {
return Icon(
index < rating ? Icons.star : Icons.star_border,
color: Colors.amber,
);
}),
),
),
);
}
}
```
### Cupertino Widgets Need Explicit Semantics
Cupertino widgets ship with weaker semantic defaults than their Material equivalents. Always wrap them.
```dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class AccessibleCupertinoSwitch extends StatelessWidget {
const AccessibleCupertinoSwitch({
required this.label,
required this.value,
required this.onChanged,
super.key,
});
final String label;
final bool value;
final ValueChanged<bool> onChanged;
@override
Widget build(BuildContext context) {
return Semantics(
label: label,
toggled: value,
enabled: true,
child: ExcludeSemantics(
child: CupertinoSwitch(value: value, onChanged: onChanged),
),
);
}
}
class AccessibleCupertinoSlider extends StatelessWidget {
const AccessibleCupertinoSlider({
required this.label,
required this.value,
required this.min,
required this.max,
required this.onChanged,
super.key,
});
final String label;
final double value;
final double min;
final double max;
final ValueChanged<double> onChanged;
@override
Widget build(BuildContext context) {
return Semantics(
label: label,
value: value.toStringAsFixed(0),
slider: true,
child: ExcludeSemantics(
child: CupertinoSlider(
value: value,
min: min,
max: max,
onChanged: onChanged,
),
),
);
}
}
```
### MergeSemantics: correct and incorrect usage
```dart
// CORRECT — merge a static label/value pair into one announcement
MergeSemantics(
child: Row(
children: [Text(label), Text(value)],
),
)
// WRONG — merging an interactive child folds away its role
MergeSemantics(
child: Row(
children: [Text(item.name), IconButton(onPressed: _delete, icon: const Icon(Icons.delete))],
),
)
// CORRECT — merge only the static content, keep the button independently focusable
Row(
children: [
MergeSemantics(child: Row(children: [Text(item.name), Text(item.subtitle)])),
IconButton(onPressed: _delete, icon: const Icon(Icons.delete), tooltip: 'Delete ${item.name}'),
],
)
```
### Live Region for Async Status Updates
```dart
import 'package:flutter/material.dart';
enum UploadStatus { idle, uploading, success, error }
class UploadStatusIndicator extends StatelessWidget {
const UploadStatusIndicator({required this.status, super.key});
final UploadStatus status;
@override
Widget build(BuildContext context) {
final icon = switch (status) {
UploadStatus.idle => const SizedBox.shrink(),
UploadStatus.uploading => const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
UploadStatus.success => const Icon(Icons.check_circle, color: Colors.green),
UploadStatus.error => const Icon(Icons.error, color: Colors.red),
};
final label = switch (status) {
UploadStatus.idle => '',
UploadStatus.uploading => 'Uploading...',
UploadStatus.success => 'Upload complete',
UploadStatus.error => 'Upload failed',
};
return Semantics(
liveRegion: true,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [icon, const SizedBox(width: 8), Text(label)],
),
);
}
}
```
For one-shot announcements:
```dart
SemanticsService.announce('Item added to cart', TextDirection.ltr);
```
---
## Touch Targets & Dragging Alternatives — Extended Examples
### Expanding Small Icons to Meet Minimum Size
```dart
import 'package:flutter/material.dart';
/// Wraps any small widget in a minimum 48x48 touch target.
/// 48 is the VGV recommended minimum. The WCAG 2.2 AA floor (2.5.8) is 24.
class AccessibleTapTarget extends StatelessWidget {
const AccessibleTapTarget({
required this.onTap,
required this.semanticLabel,
required this.child,
super.key,
});
final VoidCallback onTap;
final String semanticLabel;
final Widget child;
@override
Widget build(BuildContext context) {
return Semantics(
label: semanticLabel,
button: true,
child: InkWell(
onTap: onTap,
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 48, minHeight: 48),
child: Center(child: child),
),
),
);
}
}
// Usage
AccessibleTapTarget(
onTap: _onClose,
semanticLabel: 'Close dialog',
child: const Icon(Icons.close, size: 16),
)
```
### Dismissible With a Non-Drag Alternative (WCAG 2.2 2.5.7)
```dart
class AccessibleDismissibleListItem extends StatelessWidget {
const AccessibleDismissibleListItem({
required this.item,
required this.onDelete,
super.key,
});
final Item item;
final ValueChanged<Item> onDelete;
@override
Widget build(BuildContext context) {
return Dismissible(
key: ValueKey(item.id),
direction: DismissDirection.endToStart,
background: Container(color: Colors.red.shade400),
onDismissed: (_) => onDelete(item),
child: ListTile(
title: Text(item.name),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
tooltip: 'Delete ${item.name}',
onPressed: () => onDelete(item),
),
),
);
}
}
```
### Reorderable List With Up/Down Buttons (WCAG 2.2 2.5.7)
```dart
class AccessibleReorderableList extends StatelessWidget {
const AccessibleReorderableList({
required this.items,
required this.onReorder,
super.key,
});
final List<Item> items;
final void Function(int oldIndex, int newIndex) onReorder;
@override
Widget build(BuildContext context) {
return ReorderableListView.builder(
itemCount: items.length,
onReorder: onReorder,
itemBuilder: (context, index) {
final item = items[index];
final isFirst = index == 0;
final isLast = index == items.length - 1;
return ListTile(
key: ValueKey(item.id),
title: Text(item.name),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.arrow_upward),
tooltip: 'Move up',
onPressed: isFirst ? null : () => onReorder(index, index - 1),
),
IconButton(
icon: const Icon(Icons.arrow_downward),
tooltip: 'Move down',
// ReorderableListView's onReorder uses index + 1 when moving down
onPressed: isLast ? null : () => onReorder(index, index + 2),
),
const Icon(Icons.drag_handle),
],
),
);
},
);
}
}
```
### Slider With Keyboard Stepping (WCAG 2.2 2.5.7)
Material's `Slider` already responds to arrow keys. Custom slider widgets must do the same.
```dart
class AccessibleSlider extends StatelessWidget {
const AccessibleSlider({
required this.label,
required this.value,
required this.onChanged,
this.min = 0,
this.max = 100,
this.step = 1,
super.key,
});
final String label;
final double value;
final double min;
final double max;
final double step;
final ValueChanged<double> onChanged;
@override
Widget build(BuildContext context) {
return Row(
children: [
IconButton(
icon: const Icon(Icons.remove),
tooltip: 'Decrease ${label.toLowerCase()}',
onPressed: value > min ? () => onChanged((value - step).clamp(min, max)) : null,
),
Expanded(
child: Semantics(
label: label,
value: value.toStringAsFixed(0),
slider: true,
child: Slider(value: value, min: min, max: max, onChanged: onChanged),
),
),
IconButton(
icon: const Icon(Icons.add),
tooltip: 'Increase ${label.toLowerCase()}',
onPressed: value < max ? () => onChanged((value + step).clamp(min, max)) : null,
),
],
);
}
}
```
---
## Focus & Keyboard — Extended Examples
### Custom Focus Traversal for a Form
```dart
import 'package:flutter/material.dart';
class AccessibleForm extends StatelessWidget {
const AccessibleForm({super.key});
@override
Widget build(BuildContext context) {
return FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
children: [
FocusTraversalOrder(
order: const NumericFocusOrder(1),
child: TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
textInputAction: TextInputAction.next,
),
),
const SizedBox(height: 16),
FocusTraversalOrder(
order: const NumericFocusOrder(2),
child: TextFormField(
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
autofillHints: const [AutofillHints.password],
textInputAction: TextInputAction.done,
),
),
const SizedBox(height: 24),
FocusTraversalOrder(
order: const NumericFocusOrder(3),
child: ElevatedButton(
onPressed: () {},
child: const Text('Sign In'),
),
),
],
),
);
}
}
```
### Focus Not Obscured: ensureVisible on Sticky-Header Layouts (WCAG 2.2 2.4.11)
```dart
class FocusEnsureVisibleField extends StatefulWidget {
const FocusEnsureVisibleField({
required this.label,
required this.stickyHeaderHeight,
super.key,
});
final String label;
final double stickyHeaderHeight;
@override
State<FocusEnsureVisibleField> createState() => _FocusEnsureVisibleFieldState();
}
class _FocusEnsureVisibleFieldState extends State<FocusEnsureVisibleField> {
final _focusNode = FocusNode();
@override
void initState() {
super.initState();
_focusNode.addListener(_handleFocus);
}
void _handleFocus() {
if (_focusNode.hasFocus) {
Scrollable.ensureVisible(
context,
alignment: 0.5,
duration: const Duration(milliseconds: 200),
);
}
}
@override
void dispose() {
_focusNode.removeListener(_handleFocus);
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
// Reserve space below the sticky header so the focused field clears it.
padding: EdgeInsets.only(top: widget.stickyHeaderHeight + 8),
child: TextField(
focusNode: _focusNode,
decoration: InputDecoration(labelText: widget.label),
),
);
}
}
```
### Focus Appearance at AAA (WCAG 2.2 2.4.13)
```dart
class FocusAppearanceWrap extends StatefulWidget {
const FocusAppearanceWrap({
required this.onPressed,
required this.child,
super.key,
});
final VoidCallback onPressed;
final Widget child;
@override
State<FocusAppearanceWrap> createState() => _FocusAppearanceWrapState();
}
class _FocusAppearanceWrapState extends State<FocusAppearanceWrap> {
bool _focused = false;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return FocusableActionDetector(
onFocusChange: (hasFocus) => setState(() => _focused = hasFocus),
actions: {
ActivateIntent: CallbackAction<ActivateIntent>(
onInvoke: (_) {
widget.onPressed();
return null;
},
),
},
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: widget.onPressed,
child: AnimatedContainer(
duration: const Duration(milliseconds: 100),
decoration: BoxDecoration(
border: Border.all(
width: _focused ? 2 : 0,
color: _focused ? scheme.primary : Colors.transparent,
),
borderRadius: BorderRadius.circular(8),
),
child: widget.child,
),
),
);
}
}
```
---
## Color Contrast — Extended Examples
### Contrast-Safe Theme
```dart
import 'package:flutter/material.dart';
ThemeData buildAccessibleTheme() {
const colorScheme = ColorScheme(
brightness: Brightness.light,
primary: Color(0xFF1565C0), // Blue 800
onPrimary: Color(0xFFFFFFFF), // White, 8.6:1 on primary
secondary: Color(0xFF00695C), // Teal 800
onSecondary: Color(0xFFFFFFFF), // White, 7.1:1 on secondary
error: Color(0xFFB71C1C), // Red 900
onError: Color(0xFFFFFFFF), // White, 7.8:1 on error
surface: Color(0xFFFFFFFF), // White
onSurface: Color(0xFF212121), // Grey 900, 16:1 on white
);
return ThemeData(
colorScheme: colorScheme,
textTheme: const TextTheme(
bodyLarge: TextStyle(fontSize: 16, height: 1.5),
bodyMedium: TextStyle(fontSize: 14, height: 1.5),
),
);
}
```
### Status Indicators Without Color Dependency
```dart
class AccessibleStatusBadge extends StatelessWidget {
const AccessibleStatusBadge({required this.status, super.key});
final TaskStatus status;
@override
Widget build(BuildContext context) {
final (icon, label, color) = switch (status) {
TaskStatus.pending => (Icons.hourglass_empty, 'Pending', Colors.orange),
TaskStatus.active => (Icons.play_circle, 'Active', Colors.blue),
TaskStatus.complete => (Icons.check_circle, 'Complete', Colors.green),
TaskStatus.error => (Icons.error, 'Error', Colors.red),
};
// Color is never the sole indicator. Icon + label always present.
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 4),
Text(label),
],
);
}
}
enum TaskStatus { pending, active, complete, error }
```
---
## Text Scaling — Extended Examples
### Adaptive Card Layout (Holds at 2x Android, 3x iOS)
```dart
class AdaptiveInfoCard extends StatelessWidget {
const AdaptiveInfoCard({
required this.title,
required this.description,
super.key,
});
final String title;
final String description;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// No fixed height. Text grows freely.
Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
// ConstrainedBox with minHeight, never fixed height.
ConstrainedBox(
constraints: const BoxConstraints(minHeight: 40),
child: Text(
description,
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
),
),
);
}
}
```
---
## Animation & Motion — Extended Examples
### Animated Page Transition Respecting Reduced Motion
```dart
import 'package:flutter/material.dart';
class AccessiblePageRoute<T> extends MaterialPageRoute<T> {
AccessiblePageRoute({required super.builder, super.settings});
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
if (MediaQuery.of(context).disableAnimations) {
return child; // Instant page change
}
return super.buildTransitions(context, animation, secondaryAnimation, child);
}
}
```
### Hero Animation With Reduced-Motion Support
```dart
class AccessibleHero extends StatelessWidget {
const AccessibleHero({required this.tag, required this.child, super.key});
final Object tag;
final Widget child;
@override
Widget build(BuildContext context) {
if (MediaQuery.of(context).disableAnimations) {
return child;
}
return Hero(tag: tag, child: child);
}
}
```
---
## Forms, Authentication, and Help — Extended Examples
### Redundant Entry: Prefill From Bloc State (WCAG 2.2 3.3.7)
```dart
class CheckoutEmailField extends StatelessWidget {
const CheckoutEmailField({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<CheckoutBloc, CheckoutState>(
buildWhen: (prev, next) => prev.email != next.email,
builder: (context, state) {
return TextField(
controller: TextEditingController(text: state.email),
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
onChanged: (v) => context.read<CheckoutBloc>().add(EmailChanged(v)),
);
},
);
}
}
```
### Accessible Authentication: Allow Paste, Support Password Managers (WCAG 2.2 3.3.8)
```dart
class AccessiblePasswordField extends StatelessWidget {
const AccessiblePasswordField({required this.controller, super.key});
final TextEditingController controller;
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
obscureText: true,
// Default is true. Do not override to false. Paste must work.
enableInteractiveSelection: true,
autofillHints: const [AutofillHints.password],
decoration: const InputDecoration(labelText: 'Password'),
);
}
}
```
### Consistent Help: Shared Help Action (WCAG 2.2 3.2.6)
```dart
/// Use this widget on every Scaffold's appBar.actions so the help button
/// always appears in the same relative position across screens.
class ConsistentHelpAction extends StatelessWidget {
const ConsistentHelpAction({super.key});
@override
Widget build(BuildContext context) {
return IconButton(
icon: const Icon(Icons.help_outline),
tooltip: 'Help',
onPressed: () => Navigator.of(context).pushNamed('/help'),
);
}
}
```
---
## Test Helpers
### Asserting a Semantic Label Is Present
```dart
testWidgets('Close button announces its purpose', (tester) async {
await tester.pumpWidget(const _Subject());
expect(find.bySemanticsLabel('Close dialog'), findsOneWidget);
});
```
### Asserting Target Size at AA (WCAG 2.2 2.5.8)
```dart
testWidgets('Icon buttons are >= 24 dp on each axis', (tester) async {
await tester.pumpWidget(const _Subject());
final size = tester.getSize(find.byIcon(Icons.close).hitTestable());
expect(size.width, greaterThanOrEqualTo(24));
expect(size.height, greaterThanOrEqualTo(24));
});
```
### Asserting Drag Operations Have Alternatives (WCAG 2.2 2.5.7)
```dart
testWidgets('Dismissible exposes a delete button', (tester) async {
await tester.pumpWidget(const _Subject());
expect(find.byTooltip('Delete Item one'), findsOneWidget);
});
```
### Asserting Animations Respect disableAnimations
```dart
testWidgets('Hero is skipped when disableAnimations is true', (tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(accessibleNavigation: true, disableAnimations: true),
child: const _Subject(),
),
);
expect(find.byType(Hero), findsNothing);
});
```
references/platforms/android.md
# Accessibility — Android Platform Reference
Per-platform WCAG 2.2 checks for Android apps using Flutter. Read this file during Phase 3 of the audit, after Phase 2 has captured Android as a selected platform.
---
## Screen Reader & Assistive Tech
- **TalkBack** — primary screen reader. Enabled at Settings > Accessibility > TalkBack.
- **Switch Access** — hardware switch or camera-based navigation. Enabled at Settings > Accessibility > Switch Access.
- **Voice Access** — voice command control.
- **Accessibility Menu** — floating simplified control panel.
**System Accessibility Settings:**
- Font scale — up to 2x (system maximum; user choice in Settings > Accessibility > Display > Font size).
- Animator duration scale — 0–10x (Settings > Developer options > Animation duration scale).
- Color inversion — inverts on-screen colors.
---
## Audit Checks
| Category | Check | WCAG 2.2 Tie-In |
| --- | --- | --- |
| Semantics | `Stack` with absolute-positioned interactive children: traversal order under TalkBack matches visual order. Wrap with `MergeSemantics` (when grouping label/value) or `Semantics(sortKey: OrdinalSortKey(...))` (when grouping is wrong) | 1.3.2 A |
| Semantics | `InkWell(onLongPress: ...)` without `onTap`: TalkBack double-tap will not activate. Add `onTap` or use a button | 4.1.2 A |
| Semantics | `Semantics(identifier: ...)` is available for UI testing (maps to Android `resource-id`, available since Flutter 3.19) | 4.1.2 A |
| Touch | Switch Access linear order respects `FocusTraversalGroup` configuration | 2.1.1 A |
| Text | Run text-scaling simulation at 2x (Android system font scale cap) | 1.4.4 AA |
| Text | Fixed-height text containers flagged: text clips at 1.5x font scale (at 2x scale it clips sooner) | 1.4.4 AA |
| Color | Color inversion compatibility: hardcoded image colors replaced with `ImageIcon(... color: Theme.of(context).iconTheme.color)` | 1.4.1 A |
| Motion | `MediaQuery.disableAnimations` reflects animator duration scale = 0. Gate every `AnimationController`, `AnimatedContainer`, `Hero`, `PageRouteBuilder` | 2.3.3 AAA |
| Focus | Focused widget never entirely hidden by sticky headers, snackbars, or overlays. Use `Scrollable.ensureVisible` | 2.4.11 AA |
---
## Testing Tools
- **Accessibility Scanner** — Google Play app. Detects missing labels, duplicate content descriptions, small targets, low contrast.
- **Android Studio "Android Ally" plugin** — live accessibility tree alongside the running app.
- **`androidTapTargetGuideline`** — Flutter test guideline. Validates 48x48 dp minimum (Android platform standard, exceeds WCAG 2.2 2.5.8 minimum of 24x24 CSS px).
- **`flutter_test` semantics helpers** — `ensureSemantics()`, `getSemantics()`, `matchesSemantics()` for verifying semantic tree structure.
---
## Flutter-Specific Gotchas
**headingLevel is binary on Android, not hierarchical**
Flutter 3.38.0 introduced a regression in heading support. The bridge switched from `Flag.IS_HEADER` to `headingLevel > 0`, breaking apps that used the old flag. The fix wires both: `result.setHeading(hasFlag(IS_HEADER) || headingLevel > 0)`.
On Android, TalkBack does **not support heading hierarchy** — only binary heading status. Use `Semantics(header: true)` or `Semantics(headingLevel: 1)` for binary heading. Do not use numeric `headingLevel` values expecting h1-h6 hierarchy on Android.
**liveRegion works reliably on Android**
`Semantics(liveRegion: true)` on Android sends an explicit `TYPE_WINDOW_CONTENT_CHANGED` event, which TalkBack respects. No additional `SemanticsService.announce()` is required (though it doesn't hurt). This differs from iOS, where `liveRegion` alone is silent.
**identifier maps to resource-id**
`SemanticsProperties.identifier` became available in Flutter 3.19 and maps to Android's `AccessibilityNodeInfo.setViewIdResourceName`. This appears as `resource-id` in UIAutomator2, useful for UI testing:
```dart
Semantics(
identifier: 'submit-button',
child: ElevatedButton(...),
)
```
**AccessibilityFeatures.boldText requires Android 12+**
`AccessibilityFeatures.boldText` is available only on Android API 31+ (Android 12+). Check the flag before applying:
```dart
if (MediaQuery.of(context).accessibilityFeatures.boldText) {
// Apply bold weight
}
```
**disableAnimations has a startup bug on Android 12+**
The `disableAnimations` flag is not set until the user explicitly toggles the Reduce Motion setting (issue: not immediately available at app startup on Android 12+). Always gate animations, but be aware that on first run, animations may play even when the setting is enabled.
**TalkBack double-tap on InkWell without onTap**
`InkWell(onLongPress: ...)` without an `onTap` callback will not activate under TalkBack double-tap. TalkBack looks for `onTap` first. Either add `onTap` or use a button widget instead.
---
## Official References
- [Android Accessibility Guide](https://developer.android.com/guide/topics/ui/accessibility) — Android accessibility framework documentation
- [TalkBack User Guide](https://support.google.com/accessibility/android/answer/6283677) — TalkBack features and gestures
- [Material Design 3: Accessibility](https://m3.material.io/foundations/accessible-design/overview) — Material accessibility patterns
- [Flutter Accessibility Guide](https://docs.flutter.dev/ui/accessibility) — Flutter semantics and accessibility APIs
- [AccessibilityFeatures class](https://api.flutter.dev/flutter/dart-ui/AccessibilityFeatures-class.html)
- [WCAG 2.2 Understanding Document](https://www.w3.org/WAI/WCAG22/Understanding/)
references/platforms/ios.md
# Accessibility — iOS Platform Reference
Per-platform WCAG 2.2 checks for iOS apps using Flutter. Read this file during Phase 3 of the audit, after Phase 2 has captured iOS as a selected platform.
---
## Screen Reader & Assistive Tech
- **VoiceOver** — primary screen reader. Enabled at Settings > Accessibility > VoiceOver.
- **Switch Control** — hardware switch navigation. Enabled at Settings > Accessibility > Switch Control.
- **Voice Control** — voice commands. Non-actionable widgets no longer get numbered labels as of Flutter 3.32.
- **AssistiveTouch** — floating menu for custom gestures.
**System Accessibility Settings:**
- Dynamic Type — font scaling up to ~3.1x (Larger Accessibility Sizes).
- Bold Text — enables bold weight on system fonts.
- Reduce Motion — disables non-essential animations and transitions.
- Reduce Transparency — replaces frosted-glass effects with opaque containers.
---
## Audit Checks
| Category | Check | WCAG 2.2 Tie-In |
| --- | --- | --- |
| Semantics | VoiceOver rotor: every screen exposes at least one `Semantics(header: true)` widget so users can navigate by heading | 2.4.6 AA |
| Semantics | `CupertinoSwitch`, `CupertinoSlider`, `CupertinoSegmentedControl`, `CupertinoButton` wrapped in `Semantics(label:, value:, button:)` | 4.1.2 A |
| Semantics | `liveRegion: true` paired with `SemanticsService.announce()` for async updates (see Flutter-Specific Gotchas below) | 4.1.3 A, 2.3.3 AAA |
| Touch | Switch Control reaches every interactive widget. `CupertinoButton` with transparent hit areas confirmed reachable | 2.1.1 A |
| Text | Run text-scaling simulation at 3x (Larger Accessibility Sizes), not 2x | 1.4.4 AA |
| Text | `MediaQuery.boldTextOverride` honored. No hardcoded `FontWeight.w400` on copy text | 1.4.4 AA |
| Motion | `MediaQuery.disableAnimations` reflects Reduce Motion. Gate every `AnimationController`, `AnimatedContainer`, `Hero`, `PageRouteBuilder` transition on this flag | 2.3.3 AAA |
| Motion | Reduce Transparency: `BackdropFilter` and frosted-glass surfaces fall back to opaque containers when Reduce Transparency is on | 1.4.11 AA |
| Focus | Focused fields near the home indicator are not occluded by `SafeArea` boundaries or the keyboard accessory | 2.4.11 AA |
---
## Testing Tools
- **Xcode Accessibility Inspector** — "Point to Inspect" for element inspection; "Run Audit" for automated issue reports. Works with iOS Simulator and physical device.
- **`iOSTapTargetGuideline`** — Flutter test guideline. Validates 44x44 pt minimum (iOS platform standard, stricter than WCAG 2.2 2.5.8 minimum of 24x24 CSS px).
- **VoiceOver Rotor** — in VoiceOver on-device, swipe up with two fingers, then swipe right to access headings list.
---
## Flutter-Specific Gotchas
### liveRegion does not auto-announce
`Semantics(liveRegion: true)` marks a region as frequently updated but does **not automatically announce** the change on iOS (issue #45968). You must also call `SemanticsService.announce()`:
```dart
Semantics(
liveRegion: true,
child: Text('Status: $status'),
)
SemanticsService.announce('Status changed to $status', TextDirection.ltr);
```
Without the `announce()` call, VoiceOver users will not hear the update unless they manually navigate back to the live region.
### SemanticsService.announce() gets interrupted
When a button also has semantic hints (via `Semantics(hint:)`), calling `SemanticsService.announce()` during active button interaction gets interrupted (issue #122101). Debounce frequent calls and avoid calling during touch/interaction.
### headingLevel is binary on iOS, not hierarchical
Flutter 3.38.0 wired `Semantics(headingLevel: 1)` through to iOS, but the platform does not support heading hierarchy. Use `Semantics(header: true)` for binary heading (maps to `UIAccessibilityTraitHeader`). Do not use numeric `headingLevel` values expecting h1-h6 hierarchy on iOS.
### AccessibilityFeatures.highContrast and invertColors are iOS-only
These flags exist on iOS only. Check them before applying iOS-specific UI adjustments:
```dart
final features = MediaQuery.of(context).accessibilityFeatures;
if (features.highContrast) {
// Apply high-contrast colors
}
if (features.invertColors) {
// Handle inverted colors
}
```
### Full Keyboard Access bug
Enabling Full Keyboard Access (external keyboard + iOS Accessibility setting) breaks external keyboard input in Flutter apps (issues #165303, #166683). This is a platform bug, not a code issue. Document this limitation in release notes if your app targets external keyboard users.
### Voice Control does not number non-actionable widgets
As of Flutter 3.32, non-actionable widgets no longer receive numbered labels under Voice Control. Only actionable widgets (buttons, text fields) get numbers. Adjust user guidance accordingly.
---
## Official References
- [Apple HIG: Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility) — Apple's accessibility design principles
- [VoiceOver User Guide](https://www.apple.com/accessibility/voiceover/) — VoiceOver features and gestures
- [Flutter Accessibility Guide](https://docs.flutter.dev/ui/accessibility) — Flutter semantics and accessibility APIs
- [SemanticsService.announce API](https://api.flutter.dev/flutter/services/SemanticsService/announce.html)
- [AccessibilityFeatures class](https://api.flutter.dev/flutter/dart-ui/AccessibilityFeatures-class.html)
- [WCAG 2.2 Understanding Document](https://www.w3.org/WAI/WCAG22/Understanding/)
references/platforms/linux.md
# Accessibility — Linux Platform Reference
Per-platform WCAG 2.2 checks for Flutter Linux apps. Read this file during Phase 3 of the audit, after Phase 2 has captured Linux as a selected platform.
---
## Screen Reader & Assistive Tech
- **Orca** — primary screen reader for Linux. Pre-installed on many GNOME-based distributions. Available via `apt` or `dnf`.
- **Speech Dispatcher** — backend audio service for Orca.
**System Accessibility Settings:**
- Keyboard-only navigation — primary interaction modality.
- Verbosity settings — Orca verbosity can be high; avoid excessive `Semantics(label: ...)` strings.
---
## Audit Checks
| Category | Check | WCAG 2.2 Tie-In |
| --- | --- | --- |
| Semantics | Images and icons have `semanticLabel` or `Semantics(label:)` | 1.1.1 A |
| Semantics | Avoid excessive semantic labels. Orca verbosity is high by default; long or redundant labels become noisy | 4.1.2 A |
| Semantics | Form fields have associated labels via `InputDecoration(labelText:)` or explicit `Semantics(label:)` | 3.3.2 A |
| Focus | Focus indicators always visible. Desktop users navigate primarily by Tab | 2.4.7 AA |
| Focus | Focused widget never entirely hidden by sticky headers or overlays. Use `Scrollable.ensureVisible` | 2.4.11 AA |
| Keyboard | App-level `Shortcuts` widget exposing standard shortcuts (`Ctrl+K` search, `Ctrl+F` find) on screens where expected | 2.1.1 A |
| Touch | Interactive targets >= 24x24 dp (WCAG 2.2 2.5.8 AA minimum; desktop targets are often smaller than mobile) | 2.5.8 AA |
---
## Testing Tools
- **Orca** — built-in on GNOME. Toggle via Super (Windows key) + Alt + S, or via Settings > Accessibility > Screen Reader.
- **Keyboard-only testing** — use Tab, Shift+Tab, Enter, Space, arrows to navigate without a screen reader.
- **No Flutter-specific automated tooling** — manual testing with Orca is the standard approach.
---
## Flutter-Specific Gotchas
**Linux is the least mature Flutter accessibility platform**
Flutter on Linux uses GTK, which supports AT-SPI2 (Assistive Technology Service Provider Interface). However, real-world Orca compatibility has documented gaps:
- Dropdown menu content is not always readable by Orca (confirmed in Ubuntu 25.10 Flutter installer, 2025).
- Some complex widget hierarchies may not expose semantics correctly.
Treat Linux as best-effort and document known limitations.
**SemanticsRole is web-only**
`SemanticsRole` enum support is web-only in Flutter 3.32. Linux support is not confirmed. Do not rely on `SemanticsRole` for Linux accessibility.
**AccessibilityFeatures platform-specific flags not supported**
`AccessibilityFeatures` platform-specific flags (e.g., `boldText`, `highContrast`, `invertColors`) are not documented as available on Linux. Do not assume these flags work.
**Orca verbosity is high**
Orca reads out semantic labels verbosely. Avoid redundant or overly long labels. For example, instead of:
```dart
Semantics(
label: 'Delete item button. Item name is: Item one. Press Space to activate',
child: IconButton(...)
)
```
Use:
```dart
IconButton(
tooltip: 'Delete item',
onPressed: _delete,
icon: const Icon(Icons.delete),
)
```
**Focus mode only**
Like Windows, Flutter Desktop on Linux runs in **focus mode**. Heading navigation is not available. All content must be reachable via Tab navigation.
---
## Official References
- [Orca Screen Reader](https://help.gnome.org/users/orca/stable/) — GNOME Orca documentation
- [GNOME Accessibility](https://www.gnome.org/accessibility/) — GNOME accessibility overview
- [Flutter Linux Platform Channel](https://docs.flutter.dev/platform-integration/linux) — Flutter Linux-specific documentation
- [Flutter Accessibility Guide](https://docs.flutter.dev/ui/accessibility) — Flutter semantics and accessibility APIs
- [AT-SPI2 Documentation](https://www.linuxfoundation.org/projects/d-bus/) — AT-SPI2 assistive technology framework
- [WCAG 2.2 Understanding Document](https://www.w3.org/WAI/WCAG22/Understanding/)
references/platforms/macos.md
# Accessibility — macOS Platform Reference
Per-platform WCAG 2.2 checks for Flutter macOS apps. Read this file during Phase 3 of the audit, after Phase 2 has captured macOS as a selected platform.
---
## Screen Reader & Assistive Tech
- **VoiceOver** — primary screen reader. Toggle with Cmd+F5.
- **Full Keyboard Access** — enable keyboard navigation in System Preferences > Keyboard > Shortcuts. Toggle with Ctrl+F7.
**System Accessibility Settings:**
- Reduce Motion — disables non-essential animations and transitions.
- Increase Contrast — increases contrast on system UI and colors.
---
## Audit Checks
| Category | Check | WCAG 2.2 Tie-In |
| --- | --- | --- |
| Focus | All focusables have a visible focus indicator that satisfies 2.4.7 AA. Assume Full Keyboard Access is off by default but design as if it is on | 2.4.7 AA |
| Focus | Focus indicator inside dialogs: visible regardless of Full Keyboard Access state, since VoiceOver users without FKA still rely on it for sighted navigation | 2.4.7 AA, 2.4.13 AAA |
| Focus | Focused widget never entirely hidden by sticky headers, snackbars, or overlays. Use `Scrollable.ensureVisible` | 2.4.11 AA |
| Color | `MediaQuery.highContrast` reflects macOS Increase Contrast. Hardcoded `Color(0xff...)` outside `ThemeExtension` flagged | 1.4.11 AA |
| Motion | `MediaQuery.disableAnimations` reflects Reduce Motion. Gate every `AnimationController`, `AnimatedContainer`, `Hero`, `PageRouteBuilder` | 2.3.3 AAA |
| Semantics | macOS VoiceOver is keyboard-driven (VO + arrows, VO + space). The `Semantics` tree exposed is the same as iOS, but report copy notes traversal is keyboard, not swipe | 1.3.2 A |
| Semantics | Images and icons have `semanticLabel` or `Semantics(label:)` | 1.1.1 A |
| Keyboard | App-level `Shortcuts` widget exposing standard macOS shortcuts (`Cmd+K` search, `Cmd+,` settings) on screens where expected | 2.1.1 A |
---
## Testing Tools
- **Xcode Accessibility Inspector** — works for macOS apps in addition to iOS Simulator. "Point to Inspect" and "Run Audit" features available.
- **VoiceOver on Device** — press Cmd+F5 to enable and test with physical macOS or simulator.
- **Focus debugging** — use `showFocusHighlight: true` in `MaterialApp` or `CupertinoApp` during development to visualize focus bounds.
---
## Flutter-Specific Gotchas
### AccessibilityFeatures.highContrast is iOS-only
`AccessibilityFeatures.highContrast` does **not exist on macOS**. Detect macOS Increase Contrast via `MediaQuery.highContrast` or supply separate `MaterialApp.highContrastTheme`:
```dart
final isHighContrast = MediaQuery.of(context).highContrast;
if (isHighContrast) {
// Apply high-contrast theme
}
```
### VoiceOver is keyboard-driven, not swipe-based
macOS VoiceOver uses keyboard commands (VO + arrows, VO + space) to navigate, unlike iOS VoiceOver which uses swipes. Screen readers speak semantics the same way, but the modality is different. Test with actual keyboard commands, not swipes.
### SemanticsRole is web-only
`SemanticsRole` enum support is web-only in Flutter 3.32. macOS support is planned but not yet shipped. Do not rely on `SemanticsRole` for macOS accessibility.
### Focus indicator visibility is critical
Since Full Keyboard Access is off by default but users can enable it, always provide a visible focus indicator on all focusable widgets. Users without Full Keyboard Access enabled may still use keyboard navigation with assistive tech.
---
## Official References
- [Apple HIG: Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility) — Apple's accessibility design principles
- [VoiceOver User Guide](https://www.apple.com/accessibility/voiceover/) — VoiceOver features and keyboard commands
- [Flutter Accessibility Guide](https://docs.flutter.dev/ui/accessibility) — Flutter semantics and accessibility APIs
- [Flutter macOS Platform Channel](https://docs.flutter.dev/platform-integration/macos) — Flutter macOS-specific documentation
- [AccessibilityFeatures class](https://api.flutter.dev/flutter/dart-ui/AccessibilityFeatures-class.html)
- [WCAG 2.2 Understanding Document](https://www.w3.org/WAI/WCAG22/Understanding/)
references/platforms/web.md
# Accessibility — Web Platform Reference
Per-platform WCAG 2.2 checks for Flutter Web apps. Read this file during Phase 3 of the audit, after Phase 2 has captured Web as a selected platform.
---
## Screen Reader & Assistive Tech
Flutter Web renders into a Semantics-mapped DOM that works with standard screen readers:
- **NVDA + Chrome** — Windows
- **JAWS + Chrome** — Windows
- **VoiceOver + Safari** — macOS
- **TalkBack + Chrome** — Android
- **VoiceOver + Safari** — iOS
- **Orca** — Linux (limited support; known gaps with dropdown menus)
**Assistive Tech Settings (browser/OS):**
- Zoom — browser text zoom and page zoom.
- Forced colors — `prefers-forced-colors` media query (Windows High Contrast Mode).
- Reduced motion — `prefers-reduced-motion` media query (added to Flutter in 2025).
---
## Audit Checks
| Category | Check | WCAG 2.2 Tie-In |
| --- | --- | --- |
| Semantics | Semantics tree enabled. Check for `SemanticsBinding.instance.ensureSemantics()` called at app startup when `kIsWeb` is true | 1.1.1 A and beyond |
| Semantics | Bypass Blocks: skip-link as a focusable `Semantics(button: true)` at the top of the document or a `<a href="#main">` injected via `js_interop` | 2.4.1 A |
| Semantics | Page Title: `<title>` updated by router-level hook or `web` package. `SystemChrome.setApplicationSwitcherDescription` does NOT update `<title>` on Web. Flag every call | 2.4.2 A |
| Semantics | Tooltips longer than 80 characters: announced verbatim via `aria-label`. Trim or move into the body | 1.1.1 A, 4.1.2 A |
| Semantics | `Image.network` carries `semanticLabel` through to the DOM. Verify with `find.bySemanticsLabel` in `flutter test` | 1.1.1 A |
| Semantics | `SemanticsRole` enum used correctly (web-only in Flutter 3.32+; do not rely on it for mobile/desktop) | 4.1.2 A |
| Touch | Target size in CSS px directly. Floor: 24x24. Flag any tappable element with measured CSS size below 24 px on either axis | 2.5.8 AA |
| Focus | Hover/focus content (tooltips, menus): dismissable (Esc), hoverable (mouse can cross to overlay), persistent (no auto-dismiss before reading) | 1.4.13 AA |
| Focus | Custom `HtmlElementView` next to focusable Flutter widgets: confirm it does not absorb keyboard events that should reach surrounding focusables | 2.1.1 A |
| Layout | Reflow at 320 CSS px. `Row` with non-`Flexible` children and fixed-width `SizedBox` siblings inside a route subtree flagged | 1.4.10 AA |
| Language | `lang` attribute on `<html>` and on inline language switches (rare, but flagged when a screen mixes languages) | 3.1.2 AA |
| Forms | Browser autofill: `autofillHints` propagates to HTML autocomplete attribute. Required for password manager support | 1.3.5 AA, 3.3.8 AA |
| Forms | `TextFormField` still has `aria-label` (issue #151929 regression in Flutter 3.22+; verify it was not removed on your version) | 1.1.1 A, 4.1.2 A |
---
## Testing Tools
- **Chrome DevTools: Elements > Accessibility tab** — inspect ARIA attributes and semantics tree.
- **Lighthouse** — built into Chrome DevTools. Runs accessibility audits.
- **Axe DevTools** — browser extension for detailed accessibility reports.
- **Flutter semantics debug mode** — `flutter run -d chrome --dart-define=FLUTTER_WEB_DEBUG_SHOW_SEMANTICS=true` overlays semantic nodes visually.
- **Standard web testing** — all standard web accessibility tools (WAVE, etc.) work against Flutter Web's `<flt-semantics>` DOM elements.
---
## Flutter-Specific Gotchas
**Semantics disabled by default**
Accessibility is **disabled by default** on Flutter Web for performance. Enable it in `main()`:
```dart
void main() {
runApp(const MyApp());
if (kIsWeb) {
SemanticsBinding.instance.ensureSemantics();
}
}
```
Without this call, users must press an invisible button with `aria-label="Enable accessibility"` that Flutter renders. Always enable it for production.
**SemanticsRole enum is web-only**
`SemanticsRole` maps directly to ARIA roles **on web only** in Flutter 3.32+. Other platforms (mobile, desktop) do not yet support `SemanticsRole`. Do not rely on it for mobile or desktop accessibility.
Supported roles include: `tab`, `tabBar`, `tabPanel`, `dialog`, `alertDialog`, `table`, `cell`, `row`, `columnHeader`, `searchBox`, `dragHandle`, `spinButton`, `comboBox`, `menuBar`, `menu`, `menuItem`, `menuItemCheckbox`, `menuItemRadio`, `list`, `listItem`, `form`, `tooltip`, `loadingSpinner`, `progressBar`, `hotKey`, `radioGroup`, `status`, `alert`, `complementary`, `contentInfo`, `main`, `navigation`, `region`.
**aria-label removed from input elements (regression)**
A known bug in Flutter 3.22+ removed `aria-label` from `<input>` semantic elements, breaking `TextFormField` accessibility on web (issue #151929). Check your Flutter version. If affected, apply a workaround or upgrade.
**headingLevel fully wired on web**
`Semantics(headingLevel: 1)` through `headingLevel: 6` maps to `<h1>` through `<h6>` (or `aria-level` on generic elements). Heading hierarchy works on web, unlike iOS and Android where it's binary.
**AccessibilityFeatures.reduceMotion added in 2025**
Web support for `prefers-reduced-motion` media query was added recently (PR #180041, 2025). Check the Flutter version to ensure the flag is available.
**identifier maps to flt-semantics-identifier attribute**
`SemanticsProperties.identifier` maps to a `flt-semantics-identifier` DOM attribute (not a standard ARIA property). Use for custom testing, not for accessibility trees.
**scopesRoute + namesRoute requires careful ARIA wiring**
When `scopesRoute` and `namesRoute` are on separate nodes (not the same node), the ARIA strategy differs from when combined (issue #126030). Test thoroughly and refer to the issue for workarounds.
---
## Official References
- [Flutter Web Accessibility Guide](https://docs.flutter.dev/ui/accessibility/web-accessibility) — Flutter's web-specific accessibility docs
- [Flutter Accessibility Guide](https://docs.flutter.dev/ui/accessibility) — core Flutter semantics and accessibility APIs
- [ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/) — ARIA roles, states, and properties
- [WebAIM Screen Reader Testing](https://webaim.org/articles/screenreader_testing/) — screen reader testing methodology
- [Chrome Accessibility Audit Documentation](https://developer.chrome.com/docs/lighthouse/accessibility/) — Lighthouse audit details
- [SemanticsRole enum](https://api.flutter.dev/flutter/dart-ui/SemanticsRole.html)
- [WCAG 2.2 Understanding Document](https://www.w3.org/WAI/WCAG22/Understanding/)
references/platforms/windows.md
# Accessibility — Windows Platform Reference
Per-platform WCAG 2.2 checks for Flutter Windows apps. Read this file during Phase 3 of the audit, after Phase 2 has captured Windows as a selected platform.
---
## Screen Reader & Assistive Tech
- **Narrator** — built-in screen reader. Activated via Windows+Enter.
- **NVDA** — recommended free screen reader for testing. Download from [NVDA](https://www.nvaccess.org/)
- **JAWS** — commercial screen reader.
**System Accessibility Settings:**
- Windows High Contrast Mode — forces a specific color palette (Black, White, or other presets). Controlled via Settings > Ease of Access > Display > High Contrast.
- Keyboard-only navigation — primary interaction modality for assistive tech users.
---
## Audit Checks
| Category | Check | WCAG 2.2 Tie-In |
| --- | --- | --- |
| Semantics | NVDA browse mode vs focus mode: Flutter Desktop on Windows runs in **focus mode only**. Screens that rely on heading navigation as the primary discovery pattern must note this limitation | 1.3.2 A, 2.4.6 AA |
| Semantics | Images and icons have `semanticLabel` or `Semantics(label:)` | 1.1.1 A |
| Semantics | Form fields have associated labels via `InputDecoration(labelText:)` or explicit `Semantics(label:)` | 3.3.2 A |
| Color | Windows High Contrast Mode: hardcoded `Color(0xff...)` outside `ThemeExtension` flagged. Prefer `Theme.of(context).colorScheme` tokens that respond to `MediaQuery.highContrast` | 1.4.11 AA |
| Focus | Focus indicators always visible. Desktop users navigate primarily by Tab | 2.4.7 AA |
| Focus | Focused widget never entirely hidden by sticky headers or overlays. Use `Scrollable.ensureVisible` | 2.4.11 AA |
| Focus | Focus appearance (2.4.13 AAA scoped audits): indicator >= 2 CSS px perimeter, 3:1 contrast vs unfocused state | 2.4.13 AAA |
| Keyboard | App-level `Shortcuts` widget exposing `Ctrl+K` (search), `Ctrl+F` (find), `Ctrl+,` (settings) on screens where expected | 2.1.1 A |
| Touch | Interactive targets >= 24x24 dp (WCAG 2.2 2.5.8 AA minimum; desktop targets are often smaller than mobile) | 2.5.8 AA |
---
## Testing Tools
- **Narrator** — built-in, but limited. Good for quick testing. Activated via Windows+Enter.
- **NVDA** — recommended for thorough testing. Open-source, free, actively maintained. Download from [NVDA](https://www.nvaccess.org/)
- **JAWS** — commercial option. Trial available.
- **Inspect** — Windows SDK tool for inspecting UIA (UI Automation) element trees. Help understand how Flutter elements are exposed to assistive tech.
- **Keyboard-only testing** — use Tab, Shift+Tab, Enter, Space, arrows to navigate without a screen reader.
---
## Flutter-Specific Gotchas
### AccessibilityFeatures.highContrast is iOS-only
`AccessibilityFeatures.highContrast` does **not exist on Windows**. Detect Windows High Contrast Mode via `ThemeData(useSystemColors: true)`:
```dart
ThemeData(
useMaterial3: true,
useSystemColors: true, // Auto-applies Windows High Contrast
)
```
However, support for Windows forced-colors mode is incomplete (issue #75883). Always test with High Contrast enabled in Windows Settings.
### Focus mode only, no browse mode
Flutter Desktop on Windows runs in UIA **focus mode only** — unlike web where NVDA browse mode lets users skim content by headings. Screens that rely on heading navigation as the primary discovery pattern should note this limitation in findings. All content must be reachable via Tab navigation.
### SemanticsRole is web-only
`SemanticsRole` enum support is web-only in Flutter 3.32. Windows support is planned but not yet shipped. Do not rely on `SemanticsRole` for Windows accessibility.
### UIA element tree mapping
Flutter Windows uses the UIA (UI Automation) accessibility API. The semantics tree maps to UIA automation elements. Use Inspect tool to verify correct element types and properties.
### Keyboard is the primary interaction
On Windows desktop, keyboard navigation is the primary interaction pattern. Every interactive element must be reachable via Tab and activatable via Enter or Space. Test thoroughly with keyboard-only navigation.
---
## Official References
- [Windows Accessibility](https://support.microsoft.com/en-us/accessibility) — Microsoft's accessibility overview
- [NVDA Documentation](https://www.nvaccess.org/download/) — free screen reader documentation
- [Flutter Windows Platform Channel](https://docs.flutter.dev/platform-integration/windows) — Flutter Windows-specific documentation
- [Flutter Accessibility Guide](https://docs.flutter.dev/ui/accessibility) — Flutter semantics and accessibility APIs
- [UI Automation Documentation](https://learn.microsoft.com/en-us/windows/win32/winauto/entry-uiauto-win32) — Microsoft UIA framework
- [Inspect Tool Documentation](https://learn.microsoft.com/en-us/windows/win32/winauto/inspect-objects) — using Inspect to debug accessibility
- [WCAG 2.2 Understanding Document](https://www.w3.org/WAI/WCAG22/Understanding/)
references/testing.md
# Accessibility — Full Test Suite Example
A complete Flutter accessibility test suite covering semantics, touch targets, focus management, color contrast, text scaling, and animation/motion.
---
## Full Accessibility Test Suite Example
```dart
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
// --- A. Semantics & Screen Reader ---
group('Semantics', () {
testWidgets('all images have semantic labels', (tester) async {
final handle = tester.ensureSemantics();
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Image(
image: AssetImage('assets/logo.png'),
semanticLabel: 'Company logo',
),
),
),
);
final semantics = tester.getSemantics(find.byType(Image));
expect(semantics.label, isNotEmpty);
handle.dispose();
});
testWidgets('icon buttons have tooltips', (tester) async {
final handle = tester.ensureSemantics();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: IconButton(
icon: const Icon(Icons.search),
tooltip: 'Search',
onPressed: () {},
),
),
),
);
expect(find.byTooltip('Search'), findsOneWidget);
handle.dispose();
});
testWidgets('live region announces status changes', (tester) async {
final handle = tester.ensureSemantics();
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Semantics(
liveRegion: true,
child: Text('Loading complete'),
),
),
),
);
final semantics = tester.getSemantics(find.text('Loading complete'));
expect(semantics.hasFlag(SemanticsFlag.isLiveRegion), isTrue);
handle.dispose();
});
});
// --- B. Touch Target Sizes ---
group('Touch targets', () {
testWidgets('icon button meets 48dp minimum', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () {},
),
),
),
),
);
final size = tester.getSize(find.byType(IconButton));
expect(size.width, greaterThanOrEqualTo(48));
expect(size.height, greaterThanOrEqualTo(48));
});
testWidgets('text button meets 48dp minimum height', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: TextButton(
onPressed: () {},
child: const Text('Action'),
),
),
),
),
);
final size = tester.getSize(find.byType(TextButton));
expect(size.height, greaterThanOrEqualTo(48));
});
});
// --- C. Focus & Keyboard ---
group('Focus management', () {
testWidgets('dialog traps focus', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Builder(
builder: (context) => ElevatedButton(
onPressed: () => showDialog<void>(
context: context,
builder: (_) => const AlertDialog(
title: Text('Confirm'),
content: Text('Are you sure?'),
),
),
child: const Text('Open'),
),
),
),
),
);
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
// Dialog is displayed and receives focus
expect(find.text('Confirm'), findsOneWidget);
});
testWidgets('interactive elements are focusable', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Column(
children: [
ElevatedButton(onPressed: () {}, child: const Text('Button')),
InkWell(onTap: () {}, child: const Text('Link')),
],
),
),
),
);
// Both elements have Focus ancestors
final buttonFocus = Focus.of(
tester.element(find.byType(ElevatedButton)),
);
expect(buttonFocus.canRequestFocus, isTrue);
});
});
// --- D. Color Contrast ---
group('Color contrast', () {
testWidgets('error state uses icon and label, not just color',
(tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Row(
children: [
Icon(Icons.error, color: Colors.red),
SizedBox(width: 8),
Text('Invalid input'),
],
),
),
),
);
// Both icon and text are present — not color alone
expect(find.byIcon(Icons.error), findsOneWidget);
expect(find.text('Invalid input'), findsOneWidget);
});
});
// --- E. Text Scaling ---
group('Text scaling', () {
testWidgets('text container uses minHeight, not fixed height',
(tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: ConstrainedBox(
constraints: BoxConstraints(minHeight: 48),
child: Text('Scalable text'),
),
),
),
);
// At default scale, widget renders
expect(find.text('Scalable text'), findsOneWidget);
});
testWidgets('text is not clipped at 2x scale', (tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(textScaler: TextScaler.linear(2)),
child: const MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: 48),
child: Text('This text should not be clipped'),
),
),
),
),
),
);
expect(find.text('This text should not be clipped'), findsOneWidget);
});
});
// --- F. Animation & Motion ---
group('Animation & motion', () {
testWidgets('animations are disabled when disableAnimations is true',
(tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(disableAnimations: true),
child: MaterialApp(
home: Scaffold(
body: Builder(
builder: (context) {
final disabled =
MediaQuery.of(context).disableAnimations;
return AnimatedContainer(
duration: disabled
? Duration.zero
: const Duration(milliseconds: 300),
color: Colors.blue,
child: const Text('Animated'),
);
},
),
),
),
),
);
expect(find.text('Animated'), findsOneWidget);
});
});
}
```
references/widget-mapping.md
# Accessibility — Widget-to-Accessibility Requirements Mapping
Quick-reference table mapping Flutter widgets to their accessibility requirements and recommended implementations.
---
## Widget-to-Accessibility Requirements Mapping
| Widget | Accessibility Requirement | Implementation |
| --- | --- | --- |
| `Image` | Text alternative | `semanticLabel` or `Semantics(label:)`; use `excludeFromSemantics: true` for decorative images |
| `Icon` | Text alternative | Wrap in `Semantics(label:)` or use within a widget that provides a label |
| `IconButton` | Text alternative + touch target | `tooltip` parameter (auto-provides semantic label); inherits 48dp minimum |
| `GestureDetector` | Keyboard access | Replace with `InkWell` or button widget; `GestureDetector` is pointer-only |
| `InkWell` | Semantic role | Add `Semantics(label:, button: true)` when used as a custom button |
| `ElevatedButton` | Touch target | Inherits 48dp minimum; provide descriptive `child` text |
| `TextButton` | Touch target | Inherits 48dp minimum; provide descriptive `child` text |
| `TextField` | Label | Use `InputDecoration(labelText:)` — always provide a visible label |
| `Checkbox` | Label + state | Wrap in `CheckboxListTile` for automatic label association |
| `Switch` | Label + state | Wrap in `SwitchListTile` for automatic label association |
| `Slider` | Label + value | Use `Semantics(label:, value:)` or `Slider.adaptive` |
| `DropdownButton` | Label + expanded state | Wrap in `DropdownButtonFormField` with `InputDecoration(labelText:)` |
| `AlertDialog` | Focus management | `showDialog` handles focus trapping and restoration automatically |
| `BottomSheet` | Focus management | `showModalBottomSheet` handles focus trapping and restoration automatically |
| `ListView` | Scrolling semantics | Flutter handles scroll semantics automatically; ensure list items are accessible |
| `TabBar` | Tab semantics | Flutter provides tab semantics automatically via `TabBar` + `TabBarView` |
| `AnimatedContainer` | Motion sensitivity | Gate `duration` on `MediaQuery.of(context).disableAnimations` |
| `Hero` | Motion sensitivity | Skip `Hero` when `disableAnimations` is true |
| `PageRoute` | Motion sensitivity | Override `buildTransitions` to return `child` directly when animations disabled |
---
## References
- [Flutter Accessibility Guide](https://docs.flutter.dev/ui/accessibility) — official Flutter documentation on accessibility APIs, TalkBack/VoiceOver integration, WCAG 2.2 patterns, and `Semantics` widget usage
- [WCAG 2.2 Understanding Document](https://www.w3.org/WAI/WCAG22/Understanding/) — W3C explanations of each WCAG 2.2 success criterion, including intent, examples, and sufficient techniques
- [WCAG 2.2 Quick Reference](https://www.w3.org/WAI/WCAG22/quickref/) — filterable checklist of all WCAG 2.2 success criteria by level
SKILL.md
---
name: accessibility
description: Audit or remediate Flutter widgets against WCAG 2.2 accessibility conformance levels A, AA, or AAA across iOS, Android, Web, macOS, Windows, and Linux.
when_to_use: Building, auditing, or reviewing Flutter widgets for WCAG 2.2 accessibility across multiple platforms
effort: medium
argument-hint: "[wcag-level] [platform...]"
allowed-tools: Read Glob Grep
---
# Accessibility
Flutter accessibility auditing and remediation across WCAG 2.2 conformance levels A, AA, and AAA. The skill is split into a workflow (this file) plus reference files loaded on demand:
- [`references/audit-templates.md`](references/audit-templates.md) — severity guide, report template, level-specific passed-check lists for A, AA, AA + selected AAA, and AAA. Also includes cross-platform severity adjustment table.
- [`references/examples.md`](references/examples.md) — extended Flutter code per category, including WCAG 2.2 patterns (focus-not-obscured, dragging alternatives, Cupertino semantic wrappers, MergeSemantics correctness).
- [`references/widget-mapping.md`](references/widget-mapping.md) — table mapping each Flutter widget to its accessibility requirement and the recommended implementation. Use it when auditing a specific widget.
- [`references/testing.md`](references/testing.md) — complete accessibility test suite covering semantics, touch targets, focus management, contrast, text scaling, and motion. Use it when writing the tests that lock in a remediation.
- [`references/platforms/ios.md`](references/platforms/ios.md), [`android.md`](references/platforms/android.md), [`web.md`](references/platforms/web.md), [`macos.md`](references/platforms/macos.md), [`windows.md`](references/platforms/windows.md), [`linux.md`](references/platforms/linux.md) — per-platform WCAG 2.2 checks. Load only the file(s) matching the selected platform(s).
Read whichever reference file matches the current phase. Do not duplicate its content here.
---
## Core Standards
Apply these standards to all accessibility work:
**Conformance Level** — Begin every audit by asking which WCAG 2.2 conformance level the project targets (A, AA, AA + selected AAA, or AAA). Never assume AA.
**Platform Selection** — Begin every audit by asking which of the six platforms are targeted (iOS, Android, Web, macOS, Windows, Linux). Apply the platform rules from the matching file(s) in `references/platforms/`.
**Image Semantics** (WCAG 1.1.1) — Every `Image` must have `semanticLabel`, or be wrapped in `Semantics(label:)`. Decorative images use `excludeFromSemantics: true`.
**Gesture Detector** (WCAG 2.1.1) — Never use bare `GestureDetector` for tap targets. Use `InkWell`, `ElevatedButton`, `TextButton`, or `IconButton`. `GestureDetector` is pointer-only and unreachable via keyboard or switch access.
**Target Size** (WCAG 2.5.8) — Target Size Minimum is 24x24 CSS px (≈ 24 dp) at AA. The VGV recommended minimum is 48x48 dp. Findings between 24 dp and 48 dp are flagged as VGV-style at AA, and as WCAG findings at AAA via 2.5.5 (44 dp).
**Drag Alternatives** (WCAG 2.5.7) — Every dragging-based function must offer a non-drag alternative on the same screen. Sliders need keyboard or stepper alternatives. `Dismissible` needs an explicit delete button. `ReorderableListView` needs up/down or "move to" controls.
**Focus Not Obscured** (WCAG 2.4.11) — A focused widget must not be entirely obscured by sticky headers, snackbars, bottom sheets, persistent FABs, or overlays. Use `Scrollable.ensureVisible` and `Scaffold.resizeToAvoidBottomInset: true`.
**Color Differentiation** (WCAG 1.4.1) — Never use color as the sole differentiator. Always pair color with a label, icon, or shape.
**Animation and Motion** (WCAG 2.3.3) — All animations must respect `MediaQuery.disableAnimations`. Gate every `AnimationController`, `AnimatedContainer`, `Hero` transition, and `PageRouteBuilder` transition on this flag.
**Icon Buttons** (WCAG 4.1.2) — Icon-only buttons must have a `Tooltip` or `Semantics(label:)`. Screen readers have no other way to convey purpose.
**Exclude Semantics** (WCAG 1.1.1) — Never use `ExcludeSemantics` on non-decorative content.
**Text Containers** (WCAG 1.4.4) — Fixed-height containers must not wrap `Text`. Use `minHeight` constraints. Fixed heights clip text at 1.5x font scale on Android, sooner on iOS where Larger Accessibility Sizes go to ~3.1x.
**Contrast** (WCAG 1.4.3) — All text and UI components must meet the contrast ratio for the selected WCAG level. See the WCAG Level Criteria Reference section below.
**Cupertino Semantics** (WCAG 4.1.2) — Cupertino widgets (`CupertinoSwitch`, `CupertinoSlider`, `CupertinoSegmentedControl`, `CupertinoButton`) ship with weaker semantic defaults than their Material equivalents. Always wrap them in `Semantics(label:, value:, button:)`.
**Autofill Hints** (WCAG 1.3.5) — Every `TextField` collecting structured personal data (email, username, password, name, address, phone, oneTimeCode) must declare `autofillHints`. Required for 1.3.5 at AA and the foundation for 3.3.7 Redundant Entry at A.
**Async Announcements** (WCAG 4.1.3) — Every async user-visible state change must announce itself via `Semantics(liveRegion: true)` or `SemanticsService.announce`.
---
## Workflow
Every accessibility engagement follows four phases in sequence. Do not skip Phase 1 or Phase 2.
### Phase 1: Conformance Level Selection
Use `AskUserQuestion` to ask:
```yaml
question: "Which WCAG 2.2 conformance level are you targeting?"
header: "WCAG level"
options:
- label: "A"
description: "Removes the most critical barriers. Includes the new 2.2 criteria 3.2.6 Consistent Help and 3.3.7 Redundant Entry."
- label: "AA"
description: "Standard most regulators require. Adds contrast, resize text, focus visible, plus the four new 2.2 AA criteria: 2.4.11, 2.5.7, 2.5.8, 3.3.8."
- label: "AA + selected AAA"
description: "AA across the app, plus specific AAA criteria scoped to flagged flows. Common: 1.4.6 enhanced contrast, 2.2.3 no timing, 2.4.13 focus appearance."
- label: "AAA"
description: "Full AAA. 7:1 contrast, no timing, no exceptions to keyboard. Rare for whole products."
```
If the user picks "AA + selected AAA", follow up with a free-text request for the AAA criterion IDs they want included (for example, "1.4.6, 2.2.3, 2.4.13").
**Outcome:** Record the selected level. All audit checks, criterion citations, and fix recommendations apply only the rules for that level (plus all levels below it) and any opted-in AAA criteria.
### Phase 2: Platform Selection
Use `AskUserQuestion` (multi-select if available, otherwise one question with a comma-separated reply) to ask:
```yaml
question: "Which platforms is this app targeting? Select all that apply."
header: "Platforms"
options:
- label: "iOS"
description: "VoiceOver, Switch Control, Dynamic Type up to 3.1x, Voice Control, Bold Text, Reduce Motion, Reduce Transparency."
- label: "Android"
description: "TalkBack, Switch Access, font scale up to 2x, Voice Access, color inversion."
- label: "Web"
description: "Flutter Web rendered to a Semantics-mapped DOM. NVDA + Chrome, JAWS + Chrome, VoiceOver + Safari."
- label: "macOS"
description: "VoiceOver, Full Keyboard Access, Reduce Motion, Increase Contrast."
- label: "Windows / Linux desktop"
description: "Narrator, NVDA, JAWS (Windows), Orca (Linux), Windows High Contrast Mode."
```
**Outcome:** Record the selected platforms. Load the matching file(s) from `references/platforms/` (for example, `references/platforms/ios.md` for iOS, `references/platforms/android.md` for Android). Load only the files for platforms that were selected; do not load unnecessary files.
### Phase 3: Level-Appropriate, Platform-Aware Audit
For each selected platform, audit the provided files or widgets across seven categories, in order:
1. **Semantics and Screen Reader** — Labels, roles, live regions, merge/exclude correctness, Cupertino semantic gaps, reading order under TalkBack and VoiceOver.
2. **Touch Targets and Dragging Alternatives** — WCAG 2.2 2.5.8 minimum (24 CSS px) at AA, 2.5.5 enhanced (44 CSS px) at AAA, VGV recommended 48 dp, plus 2.5.7 dragging alternatives.
3. **Focus and Keyboard Navigation** — Operability, traversal order, dialog focus trapping, focus indicators, plus 2.4.11 / 2.4.12 focus-not-obscured.
4. **Color Contrast** — Text and UI component ratios at the selected level's threshold (table below).
5. **Text Scaling** — No fixed-height text containers, no clamped text scaling. Cap simulations at 2x on Android and Web, 3x on iOS.
6. **Animation and Motion** — `disableAnimations` gating across `AnimationController`, `Hero`, `AnimatedContainer`, `PageRouteBuilder`. No content flashing above 3 Hz at AA, zero flashing at AAA.
7. **Forms, Authentication, and Help** — `autofillHints` (1.3.5, 3.3.7), accessible authentication (3.3.8 / 3.3.9), consistent placement of help mechanisms (3.2.6).
Apply only criteria active at the selected level (plus opted-in AAA) and relevant to the selected platforms.
For each finding, capture: file path and approximate line number, WCAG criterion ID + name + version (2.0 / 2.1 / 2.2), platform(s) affected, severity (CRITICAL / MAJOR / MINOR), current behavior, expected behavior, Flutter fix as a before-and-after diff.
**Outcome:** After completing all seven categories, produce the Audit Report using the template in [`references/audit-templates.md`](references/audit-templates.md). Pick the level-specific passed-check list that matches Phase 1.
### Phase 4: Remediation Scope Selection
After delivering the report, use `AskUserQuestion`:
```yaml
question: "The audit is complete. How would you like to proceed with fixes?"
header: "Fix scope"
options:
- label: "All issues"
description: "Fix every CRITICAL, MAJOR, and MINOR finding"
- label: "Critical + Major only"
description: "Fix blockers and significant barriers; skip MINOR polish items"
- label: "Critical only"
description: "Fix only what blocks assistive technology users entirely"
- label: "Specific findings"
description: "List the finding numbers you want fixed"
```
**Outcome:** Apply exactly the fixes the user selects. After applying fixes, confirm: "Fixed [N] findings ([severities]). [N remaining] remain open."
Write tests covering every fix so it cannot regress. See [`references/testing.md`](references/testing.md) for a suite spanning all seven audit categories, and [`references/widget-mapping.md`](references/widget-mapping.md) when the correct implementation for a specific widget is unclear.
---
## WCAG 2.2 Level Criteria Reference
Level AA includes all Level A criteria. Level AAA includes all Level A and AA criteria. The "Version" column flags whether the criterion is from WCAG 2.0, 2.1, or 2.2. WCAG 2.2 removed 4.1.1 Parsing.
### Level A
| WCAG ID | Version | Criterion | Flutter Check |
| --- | --- | --- | --- |
| 1.1.1 | 2.0 | Non-text Content | `semanticLabel` on images, `Semantics(label:)` on icons, `excludeFromSemantics: true` on decorative |
| 1.3.1 | 2.0 | Info and Relationships | Semantic roles. `MergeSemantics` for grouped label/value pairs only, never around interactive children |
| 1.3.2 | 2.0 | Meaningful Sequence | Reading order matches visual order. `FocusTraversalGroup` + `OrderedTraversalPolicy` |
| 1.3.3 | 2.0 | Sensory Characteristics | Instructions do not rely solely on shape, size, location, or sound |
| 1.4.1 | 2.0 | Use of Color | Color never sole differentiator |
| 2.1.1 | 2.0 | Keyboard | All functionality via keyboard or switch access. No bare `GestureDetector` |
| 2.1.2 | 2.0 | No Keyboard Trap | Focus can always be moved away |
| 2.3.1 | 2.0 | Three Flashes or Below Threshold | No content flashes > 3 times per second |
| 2.4.1 | 2.0 | Bypass Blocks | Skip-navigation mechanism. **Web only** |
| 2.4.2 | 2.0 | Page Titled | Each screen has a meaningful title. **Web: `<title>` tag** |
| 2.4.3 | 2.0 | Focus Order | Tab/focus order preserves meaning |
| 2.5.3 | 2.1 | Label in Name | Visible label text contained in accessible name |
| 3.2.6 | 2.2 | Consistent Help | Help mechanism in same relative order across screens |
| 3.3.1 | 2.0 | Error Identification | Form errors identified in text, not color alone |
| 3.3.2 | 2.0 | Labels or Instructions | All form fields have visible labels |
| 3.3.7 | 2.2 | Redundant Entry | Multi-step forms must not re-collect already-provided info unless re-entry is essential |
| 4.1.2 | 2.0 | Name, Role, Value | `Semantics(label:, button: true)`, `Tooltip`, state via `checked`, `selected`, `enabled` |
| 4.1.3 | 2.1 | Status Messages | `Semantics(liveRegion: true)`, `SemanticsService.announce()` |
### Level AA (adds these to Level A)
| WCAG ID | Version | Criterion | Flutter Check |
| --- | --- | --- | --- |
| 1.3.4 | 2.1 | Orientation | App not locked to single orientation without essential reason |
| 1.3.5 | 2.1 | Identify Input Purpose | Correct `keyboardType` and `autofillHints` |
| 1.4.3 | 2.0 | Contrast (Minimum) | Normal text 4.5:1, large text 3:1 |
| 1.4.4 | 2.0 | Resize Text | Text scales to 200% (300% on iOS) without loss |
| 1.4.5 | 2.0 | Images of Text | Use `Text`, not images of text |
| 1.4.10 | 2.1 | Reflow | Content reflows at 320 CSS px equivalent |
| 1.4.11 | 2.1 | Non-text Contrast | UI components and focus indicators 3:1 |
| 1.4.12 | 2.1 | Text Spacing | Content not lost under increased spacing |
| 1.4.13 | 2.1 | Content on Hover or Focus | Dismissable, hoverable, persistent. **Web/desktop** |
| 2.4.5 | 2.0 | Multiple Ways | More than one way to locate a screen |
| 2.4.6 | 2.0 | Headings and Labels | Descriptive. `Semantics(header: true)` for sections |
| 2.4.7 | 2.0 | Focus Visible | Keyboard focus indicator always visible |
| 2.4.11 | 2.2 | Focus Not Obscured (Minimum) | Focused component not entirely hidden by author-created content |
| 2.5.7 | 2.2 | Dragging Movements | Every drag has a single-pointer alternative |
| 2.5.8 | 2.2 | Target Size (Minimum) | Targets >= 24x24 CSS px, with documented exceptions |
| 3.1.2 | 2.0 | Language of Parts | **Web: `lang` attribute** |
| 3.2.3 | 2.0 | Consistent Navigation | Consistent across screens |
| 3.2.4 | 2.0 | Consistent Identification | Same-function components identified consistently |
| 3.3.3 | 2.0 | Error Suggestion | Suggested correction when possible |
| 3.3.4 | 2.0 | Error Prevention (Legal/Financial) | Reversible or confirmable |
| 3.3.8 | 2.2 | Accessible Authentication (Minimum) | No required cognitive function test without alternative. Allow paste, support password managers, no puzzle CAPTCHAs without alternative |
### Level AAA (adds these to A and AA)
| WCAG ID | Version | Criterion | Flutter Check |
| --- | --- | --- | --- |
| 1.4.6 | 2.0 | Contrast (Enhanced) | Normal 7:1, large 4.5:1 |
| 2.1.3 | 2.0 | Keyboard (No Exception) | No `GestureDetector` anywhere |
| 2.2.3 | 2.0 | No Timing | No time limits except real-time events |
| 2.2.6 | 2.1 | Timeouts | Inactivity warning |
| 2.3.2 | 2.0 | Three Flashes | Zero flashing |
| 2.3.3 | 2.1 | Animation from Interactions | Every animation gated on `disableAnimations` |
| 2.4.8 | 2.0 | Location | Users always know where they are |
| 2.4.9 | 2.0 | Link Purpose (Link Only) | Understandable from link text alone |
| 2.4.12 | 2.2 | Focus Not Obscured (Enhanced) | No occlusion at all, not just total occlusion |
| 2.4.13 | 2.2 | Focus Appearance | At least 2 CSS px perimeter, encloses component, 3:1 against unfocused |
| 2.5.5 | 2.1 | Target Size (Enhanced) | Targets >= 44x44 CSS px |
| 2.5.6 | 2.1 | Concurrent Input Mechanisms | No single-modality restriction |
| 3.2.5 | 2.0 | Change on Request | Context changes only on user request |
| 3.3.5 | 2.0 | Help | Context-sensitive help available |
| 3.3.6 | 2.0 | Error Prevention (All) | All submissions reversible or confirmable |
| 3.3.9 | 2.2 | Accessible Authentication (Enhanced) | No cognitive function test even with alternative |
---
## Quick Anti-Pattern Reference
Full code samples and corrected versions live in [`references/examples.md`](references/examples.md). The patterns below are the ones the skill flags most often.
### Semantics (WCAG 1.1.1, 4.1.2)
```dart
// WRONG: empty label, no label, ExcludeSemantics over actionable
Image.asset('assets/warning.png', semanticLabel: '')
Image.asset('assets/chart.png')
ExcludeSemantics(child: ElevatedButton(onPressed: _submit, child: const Text('Submit')))
```
```dart
// WRONG: Cupertino without semantic wrapper
CupertinoSwitch(value: _enabled, onChanged: _onChanged)
```
```dart
// WRONG: MergeSemantics around an interactive child folds the button's role away
MergeSemantics(
child: Row(children: [const Text('Item'), IconButton(onPressed: _delete, icon: ...)]),
)
```
### Touch Targets and Dragging (WCAG 2.5.8, 2.5.7)
```dart
// WRONG: 16x16 target, below WCAG 2.2 2.5.8 AA (24 dp)
SizedBox(width: 16, height: 16, child: GestureDetector(onTap: _onTap, child: const Icon(Icons.close, size: 16)))
```
```dart
// WRONG: Dismissible with no non-drag alternative (WCAG 2.2 2.5.7)
Dismissible(key: ValueKey(item.id), onDismissed: (_) => _delete(item), child: ListTile(title: Text(item.name)))
```
### Focus (WCAG 2.1.1, 2.4.11)
```dart
// WRONG: GestureDetector is not keyboard-accessible
GestureDetector(onTap: _onTap, child: const Text('Click me'))
```
```dart
// WRONG: bottom bar covers focused TextField (WCAG 2.2 2.4.11)
Scaffold(
resizeToAvoidBottomInset: false,
bottomNavigationBar: const BottomAppBar(child: ...),
body: ListView(children: [..., TextField(focusNode: _last), ...]),
)
```
### Text Scaling and Motion (WCAG 1.4.4, 2.3.3)
```dart
// WRONG: clips text at 1.5x font scale
SizedBox(height: 48, child: Text('Status: Ready'))
```
```dart
// WRONG: animation always plays
AnimatedContainer(duration: const Duration(milliseconds: 500), color: ..., child: child)
```
For corrected snippets, full classes (`AccessibleTapTarget`, `AccessibleSlider`, `AccessibleReorderableList`, `AccessiblePageRoute`, `AccessibleHero`), and the Cupertino semantic wrappers, see [`references/examples.md`](references/examples.md).
---
## Additional Resources
- [`references/audit-templates.md`](references/audit-templates.md) — severity guide, report template, level-specific passed-check lists, cross-platform severity table.
- [`references/examples.md`](references/examples.md) — full Flutter widget classes per category, including all WCAG 2.2 patterns.
- [`references/widget-mapping.md`](references/widget-mapping.md) — widget-to-requirement quick reference for all commonly audited Flutter widgets.
- [`references/testing.md`](references/testing.md) — full accessibility test suite example across all seven audit categories.
- [`references/platforms/ios.md`](references/platforms/ios.md), [`android.md`](references/platforms/android.md), [`web.md`](references/platforms/web.md), [`macos.md`](references/platforms/macos.md), [`windows.md`](references/platforms/windows.md), [`linux.md`](references/platforms/linux.md) — per-platform WCAG 2.2 checks and Flutter-specific gotchas.
Official references:
- [WCAG 2.2 Recommendation](https://www.w3.org/TR/WCAG22/)
- [WCAG 2.2 Understanding Document](https://www.w3.org/WAI/WCAG22/Understanding/)
- [Flutter Accessibility Guide](https://docs.flutter.dev/ui/accessibility)
- [Flutter Web Semantics](https://docs.flutter.dev/platform-integration/web/accessibility)
- [Apple HIG: Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility)
- [Material Design 3: Accessibility](https://m3.material.io/foundations/accessible-design/overview)