Accesibilidad en Flutter: guía técnica para cumplir WCAG 2.1 AA (y la EAA) sin rehacer tu app

La accesibilidad en Flutter se trabaja sobre el árbol de semántica que el framework construye en paralelo al árbol de widgets. La buena noticia: la mayoría de componentes Material y Cupertino ya son accesibles por defecto. El trabajo real no es "hacer accesible" toda la app desde cero, sino corregir los puntos donde esa semántica falta, sobra o está mal ordenada, cuidar el escalado de texto y el contraste, y verificarlo con tests automáticos. Con eso, una app Flutter puede cumplir WCAG 2.1 nivel AA —el estándar técnico detrás de la European Accessibility Act— sin reescribir la interfaz.

Escribimos esta guía porque falta la contraparte técnica: ya cubrimos el marco legal de la accesibilidad en apps y entornos digitales, pero una cosa es saber que tienes que cumplir y otra saber qué widget tocar. Aquí va lo segundo.

Qué te obliga exactamente (y para quién)

La European Accessibility Act (EAA) es aplicable desde el 28 de junio de 2025 a los nuevos productos y servicios digitales de consumo (comercio electrónico, banca, transporte, telecomunicaciones, e-books…). Su base técnica es la norma EN 301 549, que a efectos prácticos remite a WCAG 2.1 nivel AA. La aplicación es competencia de cada Estado miembro y en 2026 el enforcement se está intensificando: en junio de 2026, un tribunal francés ordenó a Carrefour hacer accesibles tanto su web como su app móvil bajo apercibimiento de sanción.

Sé honesto con el alcance antes de invertir:

  • Sí te aplica si tu app es un servicio de consumo dirigido al público en la UE (fintech, retail, movilidad, salud, reservas…).
  • Probablemente no si es una herramienta B2B estrictamente interna, un back-office o una prueba de concepto no publicada. También hay matices para microempresas de servicios.

Si estás en la primera categoría, sigue leyendo. Si estás en la segunda, la accesibilidad sigue siendo buena ingeniería, pero no es una obligación legal inmediata: prioriza en consecuencia.

Cómo funciona la accesibilidad en Flutter

Flutter no delega el renderizado en widgets nativos, así que tampoco hereda "gratis" su accesibilidad. En su lugar mantiene un semantics tree: una representación paralela de la UI, pensada para las tecnologías de asistencia, que se sincroniza con TalkBack (Android) y VoiceOver (iOS).

La mayoría de widgets de alto nivel ya aportan su semántica: un ElevatedButton se anuncia como botón, un TextField expone su rol y su valor, un Switch comunica su estado. El problema aparece con UI construida a mano —GestureDetector sobre un Container, iconos como botones, texto decorativo— donde esa información no existe hasta que la declaras.

El widget Semantics: describe lo que la vista no dice

Semantics envuelve un subárbol y le adjunta metadatos: qué es (label, roles como button/header/image), qué muestra (value), qué hará (hint) y en qué estado está (checked, selected, enabled).

// Un icono que actúa como botón: sin Semantics, TalkBack no dice nada útil.
Semantics(
  label: 'Añadir a favoritos',
  button: true,
  child: GestureDetector(
    onTap: _toggleFavorite,
    child: const Icon(Icons.favorite_border),
  ),
)

Los cuatro fallos que más vemos en auditorías

Después de revisar accesibilidad en decenas de proyectos, los problemas se repiten:

1. Sobre-etiquetado y lectura doble. El error más común no es la falta de semántica, sino el exceso. Envolver cada widget en Semantics provoca que el lector de pantalla anuncie la misma información dos veces o fragmente un elemento en varios nodos. Usa MergeSemantics para unir lo que conceptualmente es un solo control y ExcludeSemantics para lo que no aporta.

// Una fila "avatar + nombre + rol" debe leerse como un único elemento.
MergeSemantics(
  child: Row(
    children: [
      ExcludeSemantics(child: CircleAvatar(backgroundImage: avatar)),
      Column(children: [Text(nombre), Text(rol)]),
    ],
  ),
)

2. Imágenes e iconos mal declarados. Las imágenes informativas necesitan Image(semanticLabel: 'Gráfico de ventas Q2'); las puramente decorativas deben excluirse (ExcludeSemantics o semanticLabel: '') para no ensuciar la navegación. Etiquetar un icono decorativo es tan dañino como no etiquetar uno funcional.

3. Tamaños fijos que rompen con el escalado de texto. Si el usuario sube el tamaño de fuente del sistema y tú has fijado alturas o has hardcodeado fontSize, el texto se recorta. No pelees contra MediaQuery.textScalerOf(context): respétalo y usa layouts flexibles. Solo limita el escalado (nunca lo desactives del todo) en casos muy puntuales.

4. Orden de foco caótico. En layouts complejos, el orden de lectura del lector de pantalla puede no coincidir con el visual. Corrígelo con Semantics(sortKey: OrdinalSortKey(1.0)) en lugar de reordenar el árbol de widgets.

Checklist WCAG 2.1 AA ↔ qué tocar en Flutter

Criterio WCAG 2.1 AAQué significaEn Flutter
1.1.1 Contenido no textualToda imagen/icono con función tiene alternativa textualsemanticLabel / Semantics(label:); excluir decorativos
1.4.3 Contraste mínimo4.5:1 texto normal, 3:1 texto grandeRevisa la paleta; test textContrastGuideline
1.4.4 Redimensionar textoUsable hasta 200%Respeta textScaler; evita alturas fijas
2.5.5 Tamaño del objetivoÁreas táctiles suficientes (48x48 dp Android / 44x44 pt iOS)androidTapTargetGuideline, iOSTapTargetGuideline
4.1.2 Nombre, función, valorCada control expone rol, estado y nombreSemantics(button/header/checked/value:)

Testea la accesibilidad de forma automática

Aquí está la ventaja de Flutter: buena parte de esto se verifica en CI, sin auditoría manual. El paquete flutter_test incluye guidelines que fallan el build si no se cumplen.

testWidgets('Cumple las guías básicas de accesibilidad', (tester) async {
  final handle = tester.ensureSemantics();
  await tester.pumpWidget(const MyApp());

  await expectLater(tester, meetsGuideline(textContrastGuideline));
  await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
  await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));

  handle.dispose();
});

Complétalo con:

  • El inspector de semántica de DevTools (o showSemanticsDebugger: true) para ver el árbol que reciben las tecnologías de asistencia.
  • El paquete accessibility_tools, que avisa en debug de targets pequeños o labels ausentes mientras desarrollas.
  • SemanticsService.announce() para anunciar cambios dinámicos (por ejemplo, "3 resultados encontrados") que no provocan un cambio de foco.
  • Pruebas manuales reales con TalkBack y VoiceOver: ningún test automático sustituye recorrer la app con los ojos cerrados.

Si aún no tienes tests de accesibilidad en tu pipeline, encajan de forma natural junto al resto de tu suite: hablamos de cómo estructurarla en testing en Flutter a fondo.

Cuándo NO deberías priorizar esto (todavía)

Contra el discurso de "accesibilidad al 100% desde el día uno": si estás validando un prototipo que no verá un usuario real, invertir en un árbol de semántica pulido es optimización prematura. La accesibilidad rinde cuando hay producto público y usuarios reales. Dicho esto, es deuda técnica que se paga cara al final: reintroducir semántica y arreglar contrastes en una app madura cuesta mucho más que hacerlo bien desde los cimientos. El punto medio sensato: componentes de tu design system accesibles por defecto, tests de guidelines en CI desde el principio, y auditoría manual antes de cada lanzamiento público.

En resumen

La accesibilidad en Flutter no es reescribir la app: es entender el árbol de semántica, corregir los cuatro fallos habituales (sobre-etiquetado, imágenes mal declaradas, tamaños fijos, orden de foco) y blindar el resultado con tests automáticos. Cumplir WCAG 2.1 AA deja de ser un proyecto y pasa a ser parte del flujo normal de ingeniería.

Si tienes una app de consumo en la UE y no sabes en qué punto de cumplimiento estás, una auditoría técnica de accesibilidad es el primer paso concreto y acotado. En Dribba lo abordamos como parte de nuestro trabajo de high performance engineering: medir, priorizar por impacto real y arreglar sin frenar el roadmap.