skills/flutter/accessibility/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](https://docs.flutter.dev/ui/accessibility-and-internationalization/accessibility) | ## 🔑 Keywords para Invocación - `accessibility` - `a11y` - `semantic` - `screen-rea
npx skillsauth add altrupets/monorepo skills/flutter/accessibilityInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Atributo | Valor |
|----------|-------|
| ID | flutter-accessibility |
| Nivel | 🟡 Intermedio |
| Versión | 1.0.0 |
| Keywords | accessibility, a11y, semantic, screen-reader, wcag, talkback, voiceover |
| Referencia | Flutter Accessibility |
accessibilitya11ysemanticscreen-readerwcagtalkbackvoiceover@skill:accessibilityImplementa accessibility con semantic widgets y screen reader support
Setup wcag compliance para la app
Configura talkback y voiceover support
@skill:accessibility - Accessibility completa
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.
my_app/
├── lib/
│ ├── core/
│ │ └── accessibility/
│ │ ├── semantic_wrappers.dart
│ │ ├── accessibility_utils.dart
│ │ └── focus_manager.dart
│ │
│ └── main.dart
│
└── test/
└── accessibility_test.dart
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
// 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,
),
);
}
}
// 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);
}
}
// 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');
}
}
}
// 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'),
),
],
),
);
}
}
// 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));
});
}
✅ DO: 48x48 dp minimum
SizedBox(
width: 48,
height: 48,
child: IconButton(
onPressed: () {},
icon: const Icon(Icons.close),
),
);
✅ 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: () {},
),
);
✅ DO: Logical tab order
FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
children: [
FocusTraversalOrder(
order: NumericFocusOrder(1),
child: TextField(),
),
FocusTraversalOrder(
order: NumericFocusOrder(2),
child: TextField(),
),
],
),
);
// Verify Semantics widget is properly configured
Semantics(
label: 'Your label here',
enabled: true,
child: YourWidget(),
);
// Exclude child semantics if providing parent semantic
Semantics(
label: 'Custom label',
excludeSemantics: true,
child: Text('This won\'t be read'),
);
Versión: 1.0.0 Última actualización: Diciembre 2025 Total líneas: 1,100+
development
# 🔧 Skill: Native Integration (Swift/Kotlin) ## 📋 Metadata | Atributo | Valor | |----------|-------| | **ID** | `flutter-native-integration` | | **Nivel** | 🔴 Avanzado | | **Versión** | 1.0.0 | | **Keywords** | `native-integration`, `swift`, `kotlin`, `uikit`, `android-sdk`, `native-ui` | | **Referencia** | [Flutter Platform Integration](https://docs.flutter.dev/platform-integration) | ## 🔑 Keywords para Invocación Usa cualquiera de estos keywords en tus prompts para invocar este skill:
testing
# 🎨 Skill: MVVM Pattern ## 📋 Metadata | Atributo | Valor | |----------|-------| | **ID** | `flutter-mvvm-pattern` | | **Nivel** | 🟡 Intermedio | | **Versión** | 1.0.0 | | **Keywords** | `mvvm`, `model-view-viewmodel`, `provider`, `changenotifier` | ## 🔑 Keywords para Invocación Usa cualquiera de estos keywords en tus prompts para invocar este skill: - `mvvm` - `model-view-viewmodel` - `provider` - `changenotifier` - `@skill:mvvm` ### Ejemplos de Prompts ``` Crea una app de lista de ta
development
# 🎨 Skill: Arquitectura Modular ## 📋 Metadata | Atributo | Valor | |----------|-------| | **ID** | `flutter-modular-architecture` | | **Nivel** | 🔴 Avanzado | | **Versión** | 1.0.0 | | **Keywords** | `modular`, `modular-architecture`, `module`, `multi-module` | | **Referencia** | [Flutter Modular Package](https://pub.dev/packages/flutter_modular) | ## 🔑 Keywords para Invocación Usa cualquiera de estos keywords en tus prompts para invocar este skill: - `modular` - `modular-architecture`
tools
# 📱 Skill: Mobile Testing y Debugging con Flutter MCP ## 📋 Metadata | Atributo | Valor | |----------|-------| | **ID** | `flutter-mobile-testing` | | **Nivel** | 🔴 Avanzado | | **Versión** | 1.2.0 | | **Keywords** | `mobile-testing`, `integration-test`, `flutter-mcp`, `dart-mcp`, `debugging`, `logic-analysis`, `widget-inspection`, `device-testing` | | **Referencia** | [Dart and Flutter MCP server](https://docs.flutter.dev/ai/mcp-server) \| [Mobile MCP](https://github.com/mobile-next/mobile-