Addaw, ir a inicio Donar

Lección 8 de 10 · 12 min de lectura · Revisado el 26/10/2026

Apps híbridas y multiplataforma: React Native, Flutter y WebView

Buena parte de las apps que se auditan en España no están escritas en Swift ni en Kotlin, sino en React Native, Flutter, Ionic/Capacitor, .NET MAUI o directamente como una web empaquetada. Cada tecnología tiene su propia forma de alimentar la API de accesibilidad nativa, y cada una tiene puntos ciegos. Esta lección resume qué ofrece cada framework, cómo se usa y qué errores produce con más frecuencia.

El principio común: traducir a la plataforma

Todas estas tecnologías acaban hablando con UIAccessibility en iOS y con AccessibilityNodeInfo en Android. La pregunta para cada componente es: ¿qué nombre, rol, estado, valor y acciones llegan al servicio de accesibilidad nativo? Si el framework no los traduce, VoiceOver o TalkBack verán un rectángulo mudo. Por eso la verificación debe hacerse siempre con las tecnologías de apoyo reales en ambos sistemas, nunca solo en el emulador web o con la herramienta de inspección del framework.

React Native

React Native expone props de accesibilidad en sus componentes básicos (View, Text, Pressable, TouchableOpacity, Image…) que se mapean a ambas plataformas:

  • accessible={true}: agrupa la vista y sus hijos en un solo elemento accesible (equivalente a isAccessibilityElement / mergeDescendants). Pressable y los touchables lo activan por defecto.
  • accessibilityLabel: el nombre. Obligatorio en botones de icono e imágenes informativas.
  • accessibilityRole: button, link, header, image, checkbox, switch, tab, adjustable, alert, summary…
  • accessibilityState: {disabled, selected, checked, busy, expanded}; accessibilityValue: {min, max, now, text}.
  • accessibilityHint, accessibilityActions + onAccessibilityAction para acciones personalizadas.
  • accessibilityLiveRegion (Android) y AccessibilityInfo.announceForAccessibility() (ambos) para mensajes de estado.
  • importantForAccessibility="no-hide-descendants" (Android) y accessibilityElementsHidden (iOS) para ocultar; accessibilityViewIsModal (iOS) para diálogos.
  • AccessibilityInfo.isScreenReaderEnabled() y isReduceMotionEnabled() para adaptar comportamientos.
<Pressable
  accessibilityRole="button"
  accessibilityLabel={`${m.concepto}, ${m.importeAccesible}, ${m.fechaLarga}`}
  accessibilityHint="Abre el detalle del movimiento"
  accessibilityActions={[{ name: 'categorizar', label: 'Categorizar' }]}
  onAccessibilityAction={(e) => e.nativeEvent.actionName === 'categorizar' && categorizar(m)}
  onPress={() => abrir(m)}
  style={{ minHeight: 48 }}
>
  <Image source={m.icono} accessible={false} />
  <Text>{m.concepto}</Text>
  <Text>{m.importeFormateado}</Text>
</Pressable>

Errores habituales en React Native: TouchableOpacity envolviendo un icono sin accessibilityLabel; cabeceras de sección sin accessibilityRole="header"; modales de librerías de terceros que no bloquean el foco detrás; texto con allowFontScaling={false} (desactiva el escalado y rompe 1.4.4) o con maxFontSizeMultiplier demasiado bajo; librerías de navegación que no anuncian el cambio de pantalla (comprueba que el título de pantalla recibe foco o se anuncia). Las herramientas eslint-plugin-react-native-a11y y las pruebas con react-native-testing-library ayudan a detectar omisiones.

Flutter

Flutter dibuja todos los píxeles con su propio motor; no usa vistas nativas. Para la accesibilidad construye un árbol de semántica paralelo que sincroniza con la plataforma. Los widgets de Material y Cupertino lo alimentan por defecto; los personalizados necesitan el widget Semantics:

Semantics(
  label: '${m.concepto}, ${m.importeAccesible}, ${m.fechaLarga}',
  hint: 'Abre el detalle del movimiento',
  button: true,
  onTap: () => abrir(m),
  customSemanticsActions: {
    CustomSemanticsAction(label: 'Categorizar'): () => categorizar(m),
  },
  child: ExcludeSemantics(      // los hijos no generan nodos propios
    child: Row(children: [
      Image.asset(m.icono),
      Text(m.concepto),
      Text(m.importeFormateado),
    ]),
  ),
);

// Encabezado
Semantics(header: true, child: Text('Movimientos de marzo'));

// Imagen informativa
Image.asset('grafico.png', semanticLabel: 'Gastos por categoría: alimentación 40 %…');

// Anuncio de estado
SemanticsService.announce('Cita confirmada', TextDirection.ltr);

Otros elementos: MergeSemantics para unir los hijos en un nodo, ExcludeSemantics para ocultar, Semantics(sortKey: OrdinalSortKey(n)) para el orden, liveRegion: true para mensajes de estado, y MediaQuery.textScalerOf(context) para respetar el escalado (los Text lo hacen por defecto salvo que se fije textScaler). La bandera showSemanticsDebugger: true en MaterialApp muestra el árbol de semántica sobre la interfaz. Puntos débiles frecuentes: GestureDetector o InkWell sobre contenedores sin Semantics(button: true), Icon sin semanticLabel, listas largas con widgets personalizados que no exponen posición, y la versión web de Flutter, cuya accesibilidad depende de un árbol DOM «semántico» generado que debe activarse y verificarse.

Punto clave

En Flutter y en cualquier motor que pinte sus propios píxeles, la accesibilidad no se hereda de nada: si el árbol de semántica no describe un elemento, ese elemento no existe para TalkBack ni para VoiceOver. El inspector de semántica debe formar parte de la rutina de desarrollo.

Apps en WebView: Ionic, Capacitor, Cordova y PWA empaquetadas

Si la interfaz es HTML dentro de un WKWebView o WebView, se aplican íntegramente las WCAG 2.2 web: HTML semántico, ARIA, foco, contraste. Las tecnologías de apoyo nativas leen el DOM a través del motor del navegador. Particularidades:

  • El escalado de texto del sistema puede no aplicarse al WebView: en Android, WebSettings.textZoom debe seguir la escala de fuente del sistema (muchas apps lo fijan al 100 %); en iOS, el CSS debe usar font: -apple-system-body (y tamaños relativos) para seguir Dynamic Type.
  • Las barras y controles nativos que envuelven el WebView (pestañas, botón atrás) siguen las reglas nativas; el orden de foco entre la parte nativa y la web debe ser coherente.
  • Los plugins de cámara, biometría o notificaciones nativas deben tener alternativa y feedback accesible.
  • Ionic Framework incluye componentes con ARIA razonable, pero las versiones antiguas y los componentes personalizados requieren auditoría como cualquier web.
  • El título de la «pantalla» (2.4.2) debe anunciarse al navegar: en una SPA dentro de WebView, gestiona el foco y el document.title como en cualquier SPA.

.NET MAUI y otros

MAUI expone SemanticProperties.Description, SemanticProperties.Hint, SemanticProperties.HeadingLevel y SemanticScreenReader.Announce(); AutomationProperties.IsInAccessibleTree permite ocultar. Kotlin Multiplatform con Compose Multiplatform hereda la semántica de Compose en Android y la traduce a UIAccessibility en iOS, con cobertura aún en evolución; conviene verificar cada versión. Unity y otros motores de juego carecen de soporte nativo completo y requieren plugins específicos.

Buenas prácticas comunes

  1. Prefiere componentes del framework antes que controles pintados a mano; heredan rol, estado y acciones en ambas plataformas.
  2. Define una capa de componentes accesibles propia (botón, campo, tarjeta, cabecera) que ya traiga las props correctas, y prohíbe el uso directo de primitivas sin ellas.
  3. Prueba en dispositivo real en iOS y Android con VoiceOver y TalkBack; los mapeos difieren y un fallo puede aparecer solo en una plataforma.
  4. No desactives el escalado de fuente global; adapta la disposición.
  5. Audita las librerías de terceros (carruseles, selectores de fecha, gráficos, modales) antes de adoptarlas: son el origen de la mayoría de fallos no corregibles.
  6. Automatiza lo posible: reglas de lint de accesibilidad, pruebas que verifiquen la existencia de etiquetas y roles, y los auditores de Xcode/Android en el pipeline (lo verás en la lección «Accesibilidad en el flujo de desarrollo» de este curso y en el curso «Evaluación de aplicaciones móviles»).

Error frecuente

Dar por accesible una app híbrida porque «la web pasa axe». axe no ve la barra de pestañas nativa sin etiquetas, el WebView con textZoom fijado al 100 %, ni la falta de anuncio al cambiar de pantalla. La auditoría de una app híbrida combina la metodología web (dentro del WebView) con la nativa (todo lo que lo rodea).

Ejemplo

Una app de comercio electrónico en React Native usaba una librería de carrusel con autoavance y flechas sin etiqueta. Se sustituyó por un componente propio: accessibilityRole="adjustable" con accessibilityValue «Producto 2 de 6», acciones «incrementar/decrementar» mapeadas a siguiente/anterior, autoavance desactivado cuando isScreenReaderEnabled o isReduceMotionEnabled y un botón de pausa visible (2.2.2). El mismo componente funcionó en iOS y Android sin código específico.

Resumen

  • Todas las tecnologías multiplataforma acaban alimentando UIAccessibility y AccessibilityNodeInfo; la pregunta siempre es qué nombre, rol, estado y acciones llegan al servicio nativo.
  • React Native: accessible, accessibilityLabel/Role/State/Value/Hint/Actions, live regions y announceForAccessibility; no desactives allowFontScaling.
  • Flutter: widget Semantics (label, hint, button, header, liveRegion, custom actions), MergeSemantics/ExcludeSemantics, SemanticsService.announce y el depurador de semántica.
  • WebView: WCAG web íntegras más escalado de texto del sistema y coherencia con la parte nativa.
  • Capa de componentes accesibles propia, auditoría de librerías de terceros y pruebas en dispositivo real en ambas plataformas.

Comprueba lo que has aprendido

Esta lección se puede leer libremente. Para hacer su test de 3 preguntas, los ejercicios prácticos con páginas reales, guardar tu avance y optar al examen y al certificado, matricúlate en el curso.

Matricularme · nivel 6 (119 €)