Agent skill
accessibility-altrupets-monorepo
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/accessibility-altrupets-monorepo
SKILL.md
♿ Skill: Accessibility (a11y)
📋 Metadata
| Atributo | Valor |
|---|---|
| ID | flutter-accessibility |
| Nivel | 🟡 Intermedio |
| Versión | 1.0.0 |
| Keywords | accessibility, a11y, semantic, screen-reader, wcag, talkback, voiceover |
| Referencia | Flutter Accessibility |
🔑 Keywords para Invocación
accessibilitya11ysemanticscreen-readerwcagtalkbackvoiceover@skill:accessibility
Ejemplos de Prompts
Implementa accessibility con semantic widgets y screen reader support
Setup wcag compliance para la app
Configura talkback y voiceover support
@skill:accessibility - Accessibility completa
📖 Descripción
Este skill cubre la implementación de accesibilidad en Flutter apps siguiendo las guías WCAG (Web Content Accessibility Guidelines). Incluye Semantic widgets, screen reader support (TalkBack/VoiceOver), focus management, y contrast ratios.
⚠️ IMPORTANTE: Todos los comandos de este skill deben ejecutarse desde la raíz del proyecto (donde existe el directorio mobile/). El skill incluye verificaciones para asegurar que se está en el directorio correcto antes de ejecutar cualquier comando.
✅ Cuándo Usar Este Skill
- Siempre - La accesibilidad debe ser una prioridad
- Apps para público general
- Apps gubernamentales (requerido legalmente)
- Apps educativas
- Apps de salud
- E-commerce
- Cumplimiento legal (ADA, Section 508)
❌ Cuándo NO Usar Este Skill
- Nunca - La accesibilidad es fundamental para todos los usuarios
🏗️ Estructura del Proyecto
my_app/
├── lib/
│ ├── core/
│ │ └── accessibility/
│ │ ├── semantic_wrappers.dart
│ │ ├── accessibility_utils.dart
│ │ └── focus_manager.dart
│ │
│ └── main.dart
│
└── test/
└── accessibility_test.dart
📦 Dependencias
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
💻 Implementación
1. Semantic Widgets
// lib/core/accessibility/semantic_wrappers.dart
import 'package:flutter/material.dart';
class AccessibleButton extends StatelessWidget {
final VoidCallback onPressed;
final Widget child;
final String? semanticLabel;
final String? semanticHint;
final bool enabled;
const AccessibleButton({
Key? key,
required this.onPressed,
required this.child,
this.semanticLabel,
this.semanticHint,
this.enabled = true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Semantics(
label: semanticLabel,
hint: semanticHint,
button: true,
enabled: enabled,
onTap: enabled ? onPressed : null,
child: ElevatedButton(
onPressed: enabled ? onPressed : null,
child: child,
),
);
}
}
class AccessibleTextField extends StatelessWidget {
final TextEditingController? controller;
final String? label;
final String? hint;
final String? errorText;
final bool obscureText;
final TextInputType? keyboardType;
final ValueChanged<String>? onChanged;
const AccessibleTextField({
Key? key,
this.controller,
this.label,
this.hint,
this.errorText,
this.obscureText = false,
this.keyboardType,
this.onChanged,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Semantics(
label: label,
hint: hint,
textField: true,
child: TextField(
controller: controller,
obscureText: obscureText,
keyboardType: keyboardType,
onChanged: onChanged,
decoration: InputDecoration(
labelText: label,
hintText: hint,
errorText: errorText,
// Add semantic description for error
semanticCounterText: errorText != null ? 'Error: $errorText' : null,
),
),
);
}
}
class AccessibleImage extends StatelessWidget {
final String imagePath;
final String semanticLabel;
final double? width;
final double? height;
final BoxFit? fit;
const AccessibleImage({
Key? key,
required this.imagePath,
required this.semanticLabel,
this.width,
this.height,
this.fit,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Semantics(
label: semanticLabel,
image: true,
child: Image.asset(
imagePath,
width: width,
height: height,
fit: fit,
// Screen readers will skip this since Semantics provides the label
excludeFromSemantics: true,
),
);
}
}
class AccessibleCard extends StatelessWidget {
final Widget child;
final VoidCallback? onTap;
final String? semanticLabel;
final String? semanticHint;
const AccessibleCard({
Key? key,
required this.child,
this.onTap,
this.semanticLabel,
this.semanticHint,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Semantics(
label: semanticLabel,
hint: semanticHint,
button: onTap != null,
onTap: onTap,
container: true,
child: Card(
child: InkWell(
onTap: onTap,
child: child,
),
),
);
}
}
class AccessibleListTile extends StatelessWidget {
final Widget title;
final Widget? subtitle;
final Widget? leading;
final Widget? trailing;
final VoidCallback? onTap;
final String? semanticLabel;
const AccessibleListTile({
Key? key,
required this.title,
this.subtitle,
this.leading,
this.trailing,
this.onTap,
this.semanticLabel,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return MergeSemantics(
child: ListTile(
title: Semantics(
// Exclude default semantics from title
excludeSemantics: true,
child: title,
),
subtitle: subtitle != null
? Semantics(
excludeSemantics: true,
child: subtitle,
)
: null,
leading: leading,
trailing: trailing,
onTap: onTap,
),
);
}
}
2. Accessibility Utils
// lib/core/accessibility/accessibility_utils.dart
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
class AccessibilityUtils {
// Check if screen reader is enabled
static bool get isScreenReaderEnabled {
return WidgetsBinding.instance.accessibilityFeatures.accessibleNavigation;
}
// Check if bold text is enabled
static bool get isBoldTextEnabled {
return WidgetsBinding.instance.accessibilityFeatures.boldText;
}
// Check if reduce animations is enabled
static bool get isReduceAnimationsEnabled {
return WidgetsBinding.instance.accessibilityFeatures.disableAnimations;
}
// Check if high contrast is enabled
static bool get isHighContrastEnabled {
return WidgetsBinding.instance.accessibilityFeatures.highContrast;
}
// Check contrast ratio (WCAG guidelines)
static bool meetsContrastRatio(Color foreground, Color background, {bool isLargeText = false}) {
final contrastRatio = calculateContrastRatio(foreground, background);
// WCAG AA standards:
// - Normal text: 4.5:1
// - Large text: 3:1
final requiredRatio = isLargeText ? 3.0 : 4.5;
return contrastRatio >= requiredRatio;
}
// Calculate contrast ratio between two colors
static double calculateContrastRatio(Color color1, Color color2) {
final l1 = _relativeLuminance(color1);
final l2 = _relativeLuminance(color2);
final lighter = l1 > l2 ? l1 : l2;
final darker = l1 > l2 ? l2 : l1;
return (lighter + 0.05) / (darker + 0.05);
}
// Calculate relative luminance
static double _relativeLuminance(Color color) {
final r = _linearize(color.red / 255);
final g = _linearize(color.green / 255);
final b = _linearize(color.blue / 255);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
static double _linearize(double channel) {
if (channel <= 0.03928) {
return channel / 12.92;
}
return ((channel + 0.055) / 1.055).pow(2.4).toDouble();
}
// Get accessible text size
static double getAccessibleTextSize(double defaultSize) {
final textScaleFactor = WidgetsBinding.instance.platformDispatcher.textScaleFactor;
return defaultSize * textScaleFactor;
}
// Announce to screen reader
static void announce(BuildContext context, String message) {
SemanticsService.announce(message, TextDirection.ltr);
}
// Request focus for accessibility
static void requestFocus(FocusNode node) {
node.requestFocus();
}
}
extension ColorContrastExtension on Color {
bool hasGoodContrastWith(Color other, {bool isLargeText = false}) {
return AccessibilityUtils.meetsContrastRatio(this, other, isLargeText: isLargeText);
}
}
3. Focus Management
// lib/core/accessibility/focus_manager.dart
import 'package:flutter/material.dart';
class AccessibleFocusManager {
// Focus nodes registry
static final Map<String, FocusNode> _focusNodes = {};
// Register focus node
static FocusNode registerFocusNode(String key) {
if (!_focusNodes.containsKey(key)) {
_focusNodes[key] = FocusNode();
}
return _focusNodes[key]!;
}
// Get focus node
static FocusNode? getFocusNode(String key) {
return _focusNodes[key];
}
// Request focus
static void requestFocus(String key) {
_focusNodes[key]?.requestFocus();
}
// Unfocus
static void unfocus(String key) {
_focusNodes[key]?.unfocus();
}
// Dispose all focus nodes
static void disposeAll() {
for (final node in _focusNodes.values) {
node.dispose();
}
_focusNodes.clear();
}
// Next focus
static void nextFocus(BuildContext context) {
FocusScope.of(context).nextFocus();
}
// Previous focus
static void previousFocus(BuildContext context) {
FocusScope.of(context).previousFocus();
}
}
// Accessible Form with Focus Management
class AccessibleForm extends StatefulWidget {
@override
State<AccessibleForm> createState() => _AccessibleFormState();
}
class _AccessibleFormState extends State<AccessibleForm> {
final _formKey = GlobalKey<FormState>();
final _nameFocusNode = FocusNode();
final _emailFocusNode = FocusNode();
final _passwordFocusNode = FocusNode();
@override
void dispose() {
_nameFocusNode.dispose();
_emailFocusNode.dispose();
_passwordFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
focusNode: _nameFocusNode,
decoration: const InputDecoration(
labelText: 'Name',
hintText: 'Enter your full name',
),
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
FocusScope.of(context).requestFocus(_emailFocusNode);
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your name';
}
return null;
},
),
TextFormField(
focusNode: _emailFocusNode,
decoration: const InputDecoration(
labelText: 'Email',
hintText: 'Enter your email address',
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
FocusScope.of(context).requestFocus(_passwordFocusNode);
},
validator: (value) {
if (value == null || !value.contains('@')) {
return 'Please enter a valid email';
}
return null;
},
),
TextFormField(
focusNode: _passwordFocusNode,
decoration: const InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
),
obscureText: true,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) {
_submitForm();
},
validator: (value) {
if (value == null || value.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
},
),
ElevatedButton(
onPressed: _submitForm,
child: const Text('Submit'),
),
],
),
);
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
// Announce success to screen reader
AccessibilityUtils.announce(context, 'Form submitted successfully');
// Process form
} else {
// Announce errors
AccessibilityUtils.announce(context, 'Please fix the errors in the form');
}
}
}
4. Semantic Examples
// Complete accessibility example
class AccessibilityExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Accessibility Example'),
),
body: ListView(
children: [
// Accessible header
Semantics(
header: true,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Welcome',
style: Theme.of(context).textTheme.headlineMedium,
),
),
),
// Accessible button
Padding(
padding: const EdgeInsets.all(16),
child: AccessibleButton(
onPressed: () {},
semanticLabel: 'Submit form',
semanticHint: 'Submits the form and continues to next step',
child: const Text('Submit'),
),
),
// Accessible switch
Semantics(
label: 'Enable notifications',
hint: 'Toggle to enable or disable push notifications',
toggled: true,
child: SwitchListTile(
title: const Text('Notifications'),
value: true,
onChanged: (value) {},
),
),
// Accessible slider
Semantics(
label: 'Volume',
value: '50%',
hint: 'Adjust volume from 0 to 100 percent',
increasedValue: '51%',
decreasedValue: '49%',
child: Slider(
value: 0.5,
onChanged: (value) {},
),
),
// Accessible progress indicator
Semantics(
label: 'Loading',
value: '75% complete',
child: const LinearProgressIndicator(value: 0.75),
),
// Group related items
MergeSemantics(
child: Row(
children: [
const Icon(Icons.star),
const Text('4.5'),
const Text(' out of 5 stars'),
],
),
),
// Exclude decorative elements
ExcludeSemantics(
child: Container(
width: 100,
height: 2,
color: Colors.grey,
),
),
// Live region (announces changes)
Semantics(
liveRegion: true,
child: Text('2 new messages'),
),
],
),
);
}
}
5. Testing Accessibility
// test/accessibility_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Button has proper semantics', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ElevatedButton(
onPressed: () {},
child: const Text('Submit'),
),
),
),
);
// Find button by semantic label
final semanticFinder = find.bySemanticsLabel('Submit');
expect(semanticFinder, findsOneWidget);
// Verify button semantics
final button = tester.widget<ElevatedButton>(find.byType(ElevatedButton));
expect(button.onPressed, isNotNull);
});
testWidgets('Meets minimum tap target size', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {},
child: const Text('Tap'),
),
),
),
),
);
// Minimum tap target: 48x48 logical pixels
final buttonSize = tester.getSize(find.byType(ElevatedButton));
expect(buttonSize.width, greaterThanOrEqualTo(48));
expect(buttonSize.height, greaterThanOrEqualTo(48));
});
testWidgets('Contrast ratio meets WCAG AA', (WidgetTester tester) async {
const foregroundColor = Colors.black;
const backgroundColor = Colors.white;
final contrastRatio = AccessibilityUtils.calculateContrastRatio(
foregroundColor,
backgroundColor,
);
// WCAG AA requires 4.5:1 for normal text
expect(contrastRatio, greaterThanOrEqualTo(4.5));
});
}
🎯 Mejores Prácticas
1. Minimum Touch Targets
✅ DO: 48x48 dp minimum
SizedBox(
width: 48,
height: 48,
child: IconButton(
onPressed: () {},
icon: const Icon(Icons.close),
),
);
2. Descriptive Labels
✅ DO: Provide context
Semantics(
label: 'Delete user John Doe',
hint: 'Double tap to delete this user permanently',
child: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {},
),
);
3. Focus Order
✅ DO: Logical tab order
FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
children: [
FocusTraversalOrder(
order: NumericFocusOrder(1),
child: TextField(),
),
FocusTraversalOrder(
order: NumericFocusOrder(2),
child: TextField(),
),
],
),
);
🚨 Troubleshooting
Screen Reader Not Reading
// Verify Semantics widget is properly configured
Semantics(
label: 'Your label here',
enabled: true,
child: YourWidget(),
);
Double Announcements
// Exclude child semantics if providing parent semantic
Semantics(
label: 'Custom label',
excludeSemantics: true,
child: Text('This won\'t be read'),
);
📚 Recursos
Versión: 1.0.0 Última actualización: Diciembre 2025 Total líneas: 1,100+
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?