WCAG 2.2 Level AA compliance patterns for Flutter applications including Semantics widgets, screen reader support, keyboard navigation, and color contrast requirements
Provides WCAG 2.2 Level AA compliance patterns for Flutter apps including Semantics widgets, screen reader support, keyboard navigation, and color contrast requirements.
/plugin marketplace add Kaakati/rails-enterprise-dev/plugin install reactree-flutter-dev@manifest-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Complete guide to building accessible Flutter applications that comply with WCAG 2.2 Level AA standards.
// ❌ BAD - No semantic information
IconButton(
icon: Icon(Icons.favorite),
onPressed: () => likePage(),
)
// ✅ GOOD - Semantic label provided
Semantics(
label: 'Like this page',
hint: 'Double tap to like',
button: true,
enabled: true,
child: IconButton(
icon: Icon(Icons.favorite),
onPressed: () => likePage(),
),
)
// ✅ GOOD - Using Tooltip provides semantic label
Tooltip(
message: 'Like this page',
child: IconButton(
icon: Icon(Icons.favorite),
onPressed: () => likePage(),
),
)
Semantics(
// Identification
label: 'Submit button', // What it is
hint: 'Double tap to submit form', // How to use it
value: 'Form is incomplete', // Current state
// Role
button: true,
header: false,
image: false,
link: false,
textField: false,
slider: false,
// State
enabled: isFormValid,
checked: isChecked,
selected: isSelected,
toggled: isToggled,
expanded: isExpanded,
hidden: isHidden,
// Actions
onTap: () => submitForm(),
onLongPress: () => showOptions(),
onScrollUp: () => scrollUp(),
onScrollDown: () => scrollDown(),
onIncrease: () => increase(),
onDecrease: () => decrease(),
child: ElevatedButton(
onPressed: isFormValid ? submitForm : null,
child: Text('Submit'),
),
)
// Combine multiple widgets into single semantic node
MergeSemantics(
child: Row(
children: [
Icon(Icons.star, color: Colors.yellow),
SizedBox(width: 4),
Text('4.5'),
SizedBox(width: 4),
Text('(120 reviews)'),
],
),
)
// Screen reader announces: "4.5 star rating, 120 reviews"
// Exclude decorative elements
ExcludeSemantics(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
),
child: Text('Content'),
),
)
// ✅ GOOD - Implicit label from decoration
TextField(
decoration: InputDecoration(
labelText: 'Email',
hintText: 'name@example.com',
),
)
// ✅ GOOD - Explicit semantic label
Semantics(
label: 'Email address',
hint: 'Enter your email address',
textField: true,
child: TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
),
),
)
// ✅ GOOD - Form field with validation
TextFormField(
decoration: InputDecoration(
labelText: 'Password',
helperText: 'Must be at least 8 characters',
errorText: hasError ? 'Password is too short' : null,
),
obscureText: true,
validator: (value) {
if (value == null || value.length < 8) {
return 'Password must be at least 8 characters';
}
return null;
},
)
class FormController extends GetxController {
final isSubmitting = false.obs;
final submitSuccess = false.obs;
Future<void> submitForm() async {
isSubmitting.value = true;
// Announce loading state
SemanticsService.announce(
'Submitting form',
TextDirection.ltr,
);
final result = await repository.submit();
result.fold(
(failure) {
SemanticsService.announce(
'Error: ${failure.message}',
TextDirection.ltr,
);
},
(success) {
submitSuccess.value = true;
SemanticsService.announce(
'Form submitted successfully',
TextDirection.ltr,
);
},
);
isSubmitting.value = false;
}
}
// Announce dynamic content changes
Obx(() => Semantics(
liveRegion: true,
child: Text('${controller.itemCount} items in cart'),
))
// ❌ BAD - Touch target too small
IconButton(
iconSize: 16,
icon: Icon(Icons.close),
onPressed: () => close(),
)
// ✅ GOOD - Minimum 44x44 logical pixels
IconButton(
iconSize: 24,
padding: EdgeInsets.all(10), // Total: 24 + 20 = 44
icon: Icon(Icons.close),
onPressed: () => close(),
)
// ✅ GOOD - Wrap small widgets in larger touch target
GestureDetector(
onTap: () => toggle(),
child: Container(
width: 44,
height: 44,
alignment: Alignment.center,
child: Icon(Icons.check, size: 16),
),
)
// ✅ GOOD - Adequate spacing between targets
Row(
spacing: 16, // Minimum 8px recommended
children: [
IconButton(icon: Icon(Icons.edit), onPressed: () => edit()),
IconButton(icon: Icon(Icons.delete), onPressed: () => delete()),
],
)
// ✅ GOOD - High contrast text
Text(
'Normal text',
style: TextStyle(
color: Color(0xFF212121), // #212121 on white = 16.1:1 ✓
fontSize: 16,
),
)
Text(
'Large text',
style: TextStyle(
color: Color(0xFF767676), // #767676 on white = 4.6:1 ✓
fontSize: 24,
fontWeight: FontWeight.bold,
),
)
// ❌ BAD - Insufficient contrast
Text(
'Low contrast text',
style: TextStyle(
color: Color(0xFFCCCCCC), // #CCCCCC on white = 1.6:1 ✗
),
)
// ✅ GOOD - Focus indicators with sufficient contrast
OutlinedButton(
style: OutlinedButton.styleFrom(
side: BorderSide(
color: Color(0xFF0066CC), // 3:1 contrast minimum
width: 2,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () {},
child: Text('Button'),
)
// ✅ GOOD - Form borders
TextField(
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFF757575), // 3:1 contrast
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFF0066CC),
width: 2,
),
),
),
)
// ✅ GOOD - Default focus ring
ElevatedButton(
onPressed: () {},
child: Text('Button'),
) // Flutter provides default focus indicator
// ✅ GOOD - Custom focus indicator
Focus(
child: Builder(
builder: (context) {
final isFocused = Focus.of(context).hasFocus;
return Container(
decoration: BoxDecoration(
border: isFocused
? Border.all(color: Colors.blue, width: 3)
: null,
borderRadius: BorderRadius.circular(8),
),
child: ElevatedButton(
onPressed: () {},
child: Text('Button'),
),
);
},
),
)
// ✅ GOOD - Explicit focus order with FocusTraversalGroup
FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
children: [
FocusTraversalOrder(
order: NumericFocusOrder(1.0),
child: TextField(decoration: InputDecoration(labelText: 'First')),
),
FocusTraversalOrder(
order: NumericFocusOrder(2.0),
child: TextField(decoration: InputDecoration(labelText: 'Second')),
),
FocusTraversalOrder(
order: NumericFocusOrder(3.0),
child: ElevatedButton(
onPressed: () {},
child: Text('Submit'),
),
),
],
),
)
class AccessibleDialog extends StatefulWidget {
@override
State<AccessibleDialog> createState() => _AccessibleDialogState();
}
class _AccessibleDialogState extends State<AccessibleDialog> {
final FocusScopeNode _focusScopeNode = FocusScopeNode();
@override
void initState() {
super.initState();
// Focus first element when dialog opens
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusScopeNode.requestFocus();
});
}
@override
void dispose() {
_focusScopeNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FocusScope(
node: _focusScopeNode,
child: AlertDialog(
title: Text('Confirm Action'),
content: Text('Are you sure?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'),
),
ElevatedButton(
autofocus: true, // Focus first action
onPressed: () {
// Perform action
Navigator.pop(context);
},
child: Text('Confirm'),
),
],
),
);
}
}
class KeyboardNavigableWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.space): ActivateIntent(),
LogicalKeySet(LogicalKeyboardKey.enter): ActivateIntent(),
LogicalKeySet(LogicalKeyboardKey.escape): DismissIntent(),
},
child: Actions(
actions: {
ActivateIntent: CallbackAction<ActivateIntent>(
onInvoke: (intent) => onActivate(),
),
DismissIntent: CallbackAction<DismissIntent>(
onInvoke: (intent) => onDismiss(),
),
},
child: Focus(
autofocus: true,
child: YourWidget(),
),
),
);
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
showSemanticsDebugger: true, // Enable semantic tree overlay
home: HomePage(),
);
}
}
testWidgets('Button has semantic label', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Semantics(
label: 'Submit form',
button: true,
child: ElevatedButton(
onPressed: () {},
child: Text('Submit'),
),
),
),
),
);
// Verify semantic label
expect(
tester.getSemantics(find.byType(ElevatedButton)),
matchesSemantics(
label: 'Submit form',
isButton: true,
),
);
});
testWidgets('Touch target meets minimum size', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: IconButton(
icon: Icon(Icons.close),
onPressed: () {},
),
),
);
final size = tester.getSize(find.byType(IconButton));
expect(size.width, greaterThanOrEqualTo(44));
expect(size.height, greaterThanOrEqualTo(44));
});
MergeSemantics// Announce changes
SemanticsService.announce(
'Item added to cart',
TextDirection.ltr,
assertiveness: Assertiveness.polite,
);
// Same API works on iOS
SemanticsService.announce(
'Item added to cart',
TextDirection.ltr,
);
// ❌ BAD
IconButton(
icon: Icon(Icons.favorite),
onPressed: () => like(),
)
// ✅ GOOD
Tooltip(
message: 'Like',
child: IconButton(
icon: Icon(Icons.favorite),
onPressed: () => like(),
),
)
// ❌ BAD - 24x24 too small
Icon(Icons.close, size: 24)
// ✅ GOOD - Wrapped in 44x44 button
IconButton(
icon: Icon(Icons.close),
onPressed: () => close(),
)
// ❌ BAD - Gray on white (2:1)
Text('Low contrast', style: TextStyle(color: Color(0xFFAAAAAA)))
// ✅ GOOD - Dark gray on white (7:1)
Text('Good contrast', style: TextStyle(color: Color(0xFF555555)))
// ❌ BAD - Silent update
void addToCart(Product product) {
cart.add(product);
cartCount.value++;
}
// ✅ GOOD - Announce update
void addToCart(Product product) {
cart.add(product);
cartCount.value++;
SemanticsService.announce(
'${product.name} added to cart',
TextDirection.ltr,
);
}
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.