Conceptos básicos de accesibilidad
10 ejemplos para que empieces con accesibilidad móvil - 7 básicos e 3 intermedios. Toda pantalla de producción debe pasar estas pruebas de VoiceOver y TalkBack antes de lanzarla.
Busca en todas las páginas de la documentación
10 ejemplos para que empieces con accesibilidad móvil - 7 básicos e 3 intermedios. Toda pantalla de producción debe pasar estas pruebas de VoiceOver y TalkBack antes de lanzarla.
La accesibilidad está integrada en los componentes principales de React Native. No es necesario instalar nada adicional para etiquetas, roles y anuncios en Expo SDK 57:
npx create-expo-app@latest MyA11yApp --template blank-typescript
cd MyA11yAppHabilita los lectores de pantalla en un dispositivo físico - los simuladores funcionan para pruebas de humo, pero el timing de TalkBack y el comportamiento del rotor de VoiceOver difieren en hardware real.
| Plataforma | Cómo habilitar | Gesto de prueba rápida |
|---|---|---|
| iOS | Configuración → Accesibilidad → VoiceOver | Triple clic en botón lateral (si está configurado) |
| Android | Configuración → Accesibilidad → TalkBack | Atajo de teclas de volumen (depende del dispositivo) |
Herramientas: Estos ejemplos se dirigen a Expo SDK 57 (
expo~57.0.4), React Native 0.86.0, y React 19.2.3. Las props de semántica se aplican aView,Text,Pressable,TextInput,Image, ySwitchprincipales.
Los lectores de pantalla anuncian el nombre accesible de un elemento enfocado. Si un Pressable no tiene texto visible ni etiqueta, los usuarios escuchan "button" sin contexto.
import { Pressable, Text, StyleSheet } from "react-native";
export function SaveButton({ onPress }: { onPress: () => void }) {
return (
<Pressable
accessibilityRole="button"
accessibilityLabel="Guardar cambios"
onPress={onPress}
style={styles.button}
>
<Text style={styles.label}>Guardar</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
button: { backgroundColor: "#2563eb", padding: 14, borderRadius: 8 },
label: { color: "#fff", fontWeight: "600", textAlign: "center" },
});Text a menudo se convierte en el nombre predeterminado - aun así se recomienda accessibilityLabel explícito para controles solo con iconoLos glifos carecen de sentido para la tecnología de asistencia a menos que los describas.
import { Pressable, Text, StyleSheet } from "react-native";
export function IconButton({
label,
onPress,
}: {
label: string;
onPress: () => void;
}) {
return (
<Pressable
accessibilityRole="button"
accessibilityLabel={label}
hitSlop={12}
onPress={onPress}
style={styles.hit}
>
<Text style={styles.icon} accessibilityElementsHidden>
⋮
</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
hit: { minWidth: 44, minHeight: 44, alignItems: "center", justifyContent: "center" },
icon: { fontSize: 22 },
});accessibilityElementsHidden en glifo decorativo Text evita anuncios dobles ("más, más opciones")hitSlop o minWidth / minHeight de 44 logra el objetivo táctil mínimo sin inflar el layoutaccessibilityHint solo cuando la acción no es obvia - "Abre la configuración de cuenta" para un icono de engranajeLos roles mapean a características de la plataforma - button, link, header, switch, y más.
import { Pressable, Text, View, StyleSheet } from "react-native";
export function ArticleCard({ title, onOpen }: { title: string; onOpen: () => void }) {
return (
<View style={styles.card}>
<Text accessibilityRole="header" style={styles.title}>
{title}
</Text>
<Pressable accessibilityRole="link" accessibilityLabel={`Leer ${title}`} onPress={onOpen}>
<Text style={styles.link}>Leer artículo</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
card: { padding: 16, gap: 8 },
title: { fontSize: 20, fontWeight: "700" },
link: { color: "#2563eb" },
});| Rol | Expectativa del usuario | Error común |
|---|---|---|
button | Activa una acción | Usar en Text estático |
link | Navega a contenido relacionado | Usar en eliminación destructiva |
header | Título de sección, navegación rápida | Omitir encabezados en pantallas largas |
image | Gráfico descriptivo | Dejar predeterminado en iconos decorativos |
switch | Alterna estado encendido/apagado | Usar en Pressable en lugar de Switch |
Comunica disabled, selected, checked, y expanded explícitamente - no confíes solo en el color.
import { Pressable, Text, StyleSheet } from "react-native";
type FilterChipProps = {
label: string;
selected: boolean;
onPress: () => void;
};
export function FilterChip({ label, selected, onPress }: FilterChipProps) {
return (
<Pressable
accessibilityRole="button"
accessibilityLabel={label}
accessibilityState={{ selected }}
onPress={onPress}
style={[styles.chip, selected && styles.chipSelected]}
>
<Text style={styles.chipText}>{label}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
chip: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 16, backgroundColor: "#e2e8f0" },
chipSelected: { backgroundColor: "#2563eb" },
chipText: { fontWeight: "600" },
});accessibilityState={{ selected: true }}Switch nativo, prefiere el componente en sí - conecta automáticamente el estado checkedaccessibilityState={{ disabled: isLoading }} mientras se ejecuta trabajo asincrónicoLos adornos de fondo, iconos redundantes, y divisores visuales no deben robar el enfoque.
import { Image, Text, View, StyleSheet } from "react-native";
export function PromoBanner() {
return (
<View style={styles.banner}>
<Image
source={require("./assets/sparkle.png")}
style={styles.sparkle}
accessible={false}
importantForAccessibility="no"
/>
<Text accessibilityRole="header">Venta de verano - 20% de descuento</Text>
<View
accessible={false}
importantForAccessibility="no-hide-descendants"
style={styles.rule}
/>
</View>
);
}
const styles = StyleSheet.create({
banner: { padding: 16, gap: 8 },
sparkle: { width: 24, height: 24, position: "absolute", right: 8, top: 8 },
rule: { height: 1, backgroundColor: "#cbd5e1" },
});| Prop | Plataforma | Efecto |
|---|---|---|
accessible={false} | iOS + Android | Elimina elemento del enfoque de accesibilidad |
importantForAccessibility="no" | Android | Oculta vista única |
importantForAccessibility="no-hide-descendants" | Android | Oculta vista y todos los hijos |
accessibilityElementsHidden | iOS | Oculta subárbol de VoiceOver |
Los avatares, gráficos y fotos de productos necesitan contexto; los activos puramente decorativos no.
import { Image, StyleSheet } from "react-native";
export function UserAvatar({ uri, name }: { uri: string; name: string }) {
return (
<Image
source={{ uri }}
style={styles.avatar}
accessibilityRole="image"
accessibilityLabel={`Foto de perfil de ${name}`}
/>
);
}
export function DecorativeDivider() {
return (
<Image
source={require("./assets/wave.png")}
style={styles.wave}
accessible={false}
importantForAccessibility="no"
/>
);
}
const styles = StyleSheet.create({
avatar: { width: 48, height: 48, borderRadius: 24 },
wave: { width: "100%", height: 8 },
});accessibilityLabel en imágenes - "Gráfico de barras mostrando ingresos arriba 12% en Q3"expo-imageCuando el estado cambia sin mover el enfoque, dile explícitamente a los usuarios de lector de pantalla.
import { useEffect, useState } from "react";
import { AccessibilityInfo, Pressable, Text, View, StyleSheet } from "react-native";
export function CartBadge({ count }: { count: number }) {
const [lastAnnounced, setLastAnnounced] = useState(count);
useEffect(() => {
if (count !== lastAnnounced) {
AccessibilityInfo.announceForAccessibility(
count === 0 ? "El carrito está vacío" : `${count} artículos en el carrito`,
);
setLastAnnounced(count);
}
}, [count, lastAnnounced]);
return (
<View
accessibilityRole="text"
accessibilityLabel={`Carrito, ${count} artículos`}
accessibilityLiveRegion="polite"
style={styles.badge}
>
<Text>{count}</Text>
</View>
);
}
const styles = StyleSheet.create({
badge: { minWidth: 28, minHeight: 28, borderRadius: 14, backgroundColor: "#dc2626", alignItems: "center", justifyContent: "center" },
});AccessibilityInfo.announceForAccessibility envía un mensaje de una sola vez a VoiceOver/TalkBackaccessibilityLiveRegion="polite" (Android) agrupa cambios de texto no urgentesassertive para errores y alertas sensibles al tiempo - ver Accessibility in FormsUna tarjeta con título, precio y calificación debería ser una unidad lógica - o estar deliberadamente dividida para escanear.
import { Pressable, Text, View, StyleSheet } from "react-native";
export function ProductRow({
name,
price,
rating,
onPress,
}: {
name: string;
price: string;
rating: string;
onPress: () => void;
}) {
return (
<Pressable
accessibilityRole="button"
accessibilityLabel={`${name}, ${price}, calificado ${rating} de cinco`}
onPress={onPress}
style={styles.row}
>
<View importantForAccessibility="no-hide-descendants" accessibilityElementsHidden>
<Text style={styles.name}>{name}</Text>
<Text style={styles.price}>{price}</Text>
<Text style={styles.rating}>{rating} ★</Text>
</View>
</Pressable>
);
}
const styles = StyleSheet.create({
row: { padding: 16, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: "#e2e8f0" },
name: { fontSize: 16, fontWeight: "600" },
price: { color: "#64748b" },
rating: { color: "#f59e0b" },
});Consulta AccessibilityInfo para lector y configuración de reducir movimiento - integra accesibilidad en interfaz de usuario predeterminada, no envíes un "modo accesible" separado.
import { useEffect, useState } from "react";
import { AccessibilityInfo, Text, View } from "react-native";
export function useA11yPreferences() {
const [screenReaderEnabled, setScreenReaderEnabled] = useState(false);
const [reduceMotionEnabled, setReduceMotionEnabled] = useState(false);
useEffect(() => {
AccessibilityInfo.isScreenReaderEnabled().then(setScreenReaderEnabled);
AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotionEnabled);
const screenReaderSub = AccessibilityInfo.addEventListener(
"screenReaderChanged",
setScreenReaderEnabled,
);
const motionSub = AccessibilityInfo.addEventListener(
"reduceMotionChanged",
setReduceMotionEnabled,
);
return () => {
screenReaderSub.remove();
motionSub.remove();
};
}, []);
return { screenReaderEnabled, reduceMotionEnabled };
}
export function MotionAwareHint() {
const { screenReaderEnabled } = useA11yPreferences();
return (
<View>
<Text accessibilityRole="header">Pedidos</Text>
{screenReaderEnabled ? (
<Text>Pasa el dedo hacia la derecha a través de la lista para escuchar cada pedido.</Text>
) : null}
</View>
);
}Recorre cada pantalla con VoiceOver y TalkBack usando un script repetible.
Lista de verificación por pantalla (5 minutos):
1. Activa VoiceOver / TalkBack
2. Pasa el dedo a través de cada elemento enfocable - no hay "button" sin etiqueta
3. Activa la acción principal - escucha confirmación o el enfoque se mueve
4. Rota a horizontal (tableta) - el orden sigue siendo lógico
5. Aumenta la escala de fuente al máximo - sin acciones principales recortadas
6. Activa Reducir movimiento - sin animación requerida para completar la tarea// Helper solo para desarrollo - registra el árbol en __DEV__
import { AccessibilityInfo, Pressable, Text } from "react-native";
export function DevA11yLog({ label }: { label: string }) {
if (!__DEV__) return null;
return (
<Pressable
accessibilityRole="button"
accessibilityLabel={`Registra árbol de accesibilidad para ${label}`}
onPress={() => AccessibilityInfo.isScreenReaderEnabled().then(console.log)}
>
<Text>Registra estado a11y ({label})</Text>
</Pressable>
);
}accessibilityLabel - los mismos selectores en los que confía la tecnología de asistenciaaccessible={true} (predeterminado para Text y touchables) o que contienen hijos accesibles se convierten en paradas de enfoque.testID es solo para automatización - no reemplaza etiquetas para lectores de pantalla a menos que tus pruebas lo simulen incorrectamente.| Comportamiento | VoiceOver (iOS) | TalkBack (Android) |
|---|---|---|
| Gesto de exploración | Pasa el dedo a la derecha / izquierda | Pasa el dedo a la derecha / izquierda |
| Activar | Doble toque | Doble toque |
| Modos de lectura | Rotor (encabezados, links) | Controles de lectura/granularidad |
| Pistas | accessibilityHint hablado después de etiqueta | Pista menos usada consistentemente |
| Regiones en vivo | Limitado vs web | accessibilityLiveRegion compatible |
Prueba ambas plataformas - pasar solo iOS no es suficiente para las expectativas de accesibilidad de Play Store.
Apple HIG y Material recomiendan objetivos efectivos de 44×44 pt / 48×48 dp:
<Pressable hitSlop={10} style={{ minHeight: 44, justifyContent: "center" }} />Iconos visuales pequeños están bien cuando la región de golpe se agranda.
TextInput necesita accessibilityLabel o accessibilityLabelledBy.accessible anidadas con texto superpuesto causan tartamudeo.disabled o accessibilityState={{ disabled: true }}.accessibilityViewIsModal en raíces de superposición para que el contenido de fondo se omita.eslint-plugin-react-native-a11y pierde orden de enfoque roto e pistas engañosas.testID está bien para elementos sin un nombre de usuario, pero no dejes que reemplace etiquetas faltantes.aria-* a equivalentes de accesibilidad.accessibilityLabel, accessibilityRole) para consistencia multiplataforma en SDK 57.allowFontScaling y supervivencia del layoutVersiones de stack: Esta página fue escrita para React 19.2.3, React Native 0.86.0, y Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 16 jul 2026