Componentes de Material 3 sin romper la accesibilidad
Los componentes de la biblioteca traen resuelto mucho más de lo que se ve en una captura: áreas táctiles mínimas, capas de estado, papeles semánticos, recorrido de teclado, gestión de insets y tiempos de lectura adaptados a los lectores de pantalla. Esta lección recorre el armazón de pantalla y las barras, la escala de personalización que va de los tokens a la copia del código fuente, y el inventario preciso de garantías invisibles que cada nivel de esa escala pone en riesgo, con los síntomas concretos por los que se detecta una accesibilidad rota en una interfaz que en la captura de pantalla parece impecable.
Un botón de Material parece un rectángulo con texto, y esa apariencia es engañosa hasta un punto que conviene medir. Dentro hay un área táctil que se expande por encima de su tamaño visual hasta el mínimo que exigen las guías de accesibilidad; una capa de estado que responde a la pulsación, al foco y al puntero con opacidades calibradas; un papel semántico que hace que el lector de pantalla lo anuncie como botón y no como texto; una participación en el recorrido de teclado; y unos colores que se derivan de roles con contraste garantizado, incluidos los del estado deshabilitado. Nada de eso aparece en una imagen. Y como no aparece, la personalización lo destruye sin dejar rastro visible: la pantalla sigue viéndose bien, la captura de pantalla que se adjunta a la revisión de código sigue siendo bonita, y lo único que ha cambiado es que un grupo de usuarios ya no puede utilizar la aplicación.
- Montar el armazón de una pantalla con sus barras respetando insets y relleno interior.
- Ordenar la personalización en niveles, del más barato y seguro al más caro y arriesgado.
- Enumerar las garantías invisibles que cada componente aporta y cómo se pierden.
- Detectar accesibilidad rota mediante síntomas observables, no por inspección visual.
El armazón y las barras
El armazón de pantalla coloca las barras superior e inferior, el botón de acción flotante y el anfitrión de mensajes emergentes, y resuelve la parte más delicada del reparto de insets. A cambio exige una cosa: aplicar el relleno interior que entrega al contenido. Ignorarlo es el fallo más repetido de toda la biblioteca, y su síntoma es un primer elemento oculto bajo la barra superior que la previsualización no muestra porque allí no hay barras.
Scaffold(
modifier = Modifier.nestedScroll(comportamiento.nestedScrollConnection),
topBar = { LargeTopAppBar(title = { Text(titulo) }, scrollBehavior = comportamiento) },
bottomBar = { BarraNavegacion(destino, alCambiar) },
snackbarHost = { SnackbarHost(estadoMensajes) },
) { relleno ->
LazyColumn(
modifier = Modifier.fillMaxSize().consumeWindowInsets(relleno),
contentPadding = relleno,
) {
items(elementos, key = { it.id }) { FilaElemento(it) }
}
}
Las barras superiores existen en varias alturas y con comportamientos de desplazamiento distintos: una que se retrae en cuanto empiezas a desplazar y reaparece al primer gesto contrario, y otra que colapsa hasta su altura mínima y no vuelve a expandirse hasta que la lista llega arriba del todo. La segunda es la correcta para pantallas de contenido largo, porque devolver un titular enorme a media lectura resulta desconcertante. El comportamiento se conecta mediante desplazamiento anidado, y ese detalle importa: si la lista no propaga su desplazamiento, la barra se queda inmóvil y nadie entiende por qué.
Para la navegación principal conviene evitar la elección manual entre barra inferior, raíl lateral y cajón permanente. Existe un armazón adaptativo que decide según el tamaño de ventana disponible, y usarlo es la diferencia entre una aplicación que funciona en un plegable abierto y otra que muestra una barra inferior de teléfono en una pantalla de tableta.
// Un solo declarativo; el armazon elige barra, rail o cajon segun la ventana.
NavigationSuiteScaffold(
navigationSuiteItems = {
destinos.forEach { destino ->
item(
selected = destino == actual,
onClick = { alCambiar(destino) },
icon = { Icon(destino.icono, contentDescription = destino.etiqueta) },
label = { Text(destino.etiqueta) },
)
}
},
) { Contenido(actual) }
Nótese el detalle de la descripción textual en el icono de cada destino. Es obligatoria y no decorativa: sin ella, el lector de pantalla anuncia un elemento seleccionable sin nombre, y la barra de navegación —el control más importante de la aplicación— se vuelve inutilizable para quien no ve la pantalla.
Personalizar por capas
La personalización tiene una escala, y recorrerla en orden ahorra la mayor parte de los problemas. Cada peldaño es más potente y más caro que el anterior.
1. Tokens
Cambia el tema. Un ajuste de color o de forma alcanza a todos los componentes a la vez y no rompe nada.
2. Valores por omisión
Copia los objetos de colores, formas y elevaciones de la biblioteca cambiando solo el campo que discrepa.
3. Envoltorio
Un componente propio que envuelve al de la biblioteca y fija la API que tu producto necesita.
4. Copia del código
Traes el fuente del componente a tu módulo. Último recurso: te llevas también su mantenimiento.
El segundo peldaño merece una precisión técnica. Los objetos de valores por omisión exponen funciones que construyen los conjuntos de colores de cada componente, y esas funciones tienen todos sus parámetros con valor implícito. Eso permite escribir solo el campo que cambia y heredar el resto, incluidos los colores de estado deshabilitado, que son justo los que nadie recuerda definir cuando construye el conjunto entero a mano.
// Cambia el fondo y hereda el resto, incluido el estado deshabilitado.
Button(
onClick = alConfirmar,
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.tertiary),
shape = MaterialTheme.shapes.small,
) { Text("Confirmar") }
Casi todas las copias de código fuente que se ven en producción se hicieron para cambiar un relleno o añadir un estado de carga. Ambas cosas se resuelven en el tercer peldaño con un envoltorio de veinte líneas que conserva intacto el componente de la biblioteca. La copia solo se justifica cuando necesitas alterar la estructura interna o el comportamiento, y a partir de ese día tu componente deja de recibir las correcciones de la biblioteca, incluidas las de accesibilidad.
Diálogos y hojas: superficies que se llevan el foco
Las superficies transitorias merecen atención aparte porque concentran una cantidad desproporcionada de fallos. Un diálogo de alerta de la biblioteca trae resuelto el atrapamiento del foco, el anuncio de su apertura al lector de pantalla, el cierre con la acción de retroceso, el ancho adecuado a la ventana y el oscurecimiento del fondo. Una hoja inferior modal añade a eso el arrastre, los puntos de anclaje y el respeto a los insets de la parte baja.
AlertDialog(
onDismissRequest = alCerrar,
title = { Text("Eliminar el archivo") },
text = { Text("Esta accion no se puede deshacer.") },
confirmButton = { TextButton(onClick = alConfirmar) { Text("Eliminar") } },
dismissButton = { TextButton(onClick = alCerrar) { Text("Cancelar") } },
)
Existe una variante básica pensada para cuando necesitas una estructura interna distinta: te entrega la ventana, el comportamiento de cierre y el atrapamiento de foco, y deja el contenido enteramente en tus manos. Es el peldaño correcto cuando el diálogo de la biblioteca no encaja, y es muchísimo mejor que construir una ventana emergente propia, porque conserva justo la parte que no se ve.
El síntoma es fácil de comprobar y casi nadie lo comprueba: con un lector de pantalla activo, desliza para avanzar por los elementos mientras el diálogo está abierto. Si el recorrido se escapa al contenido de debajo, el usuario queda operando una pantalla que visualmente está bloqueada. Lo mismo con el teclado y la tabulación.
Lo que se rompe sin avisar
Aquí está el inventario que hay que conocer de memoria, porque ninguno de estos fallos produce un error de compilación ni una excepción.
El área táctil mínima es el primero. Los componentes interactivos de la biblioteca reservan un área de pulsación de al menos cuarenta y ocho unidades de densidad aunque su dibujo sea menor. Un icono de veinticuatro al que le pones tú el modificador de pulsación mide veinticuatro, y su tasa de acierto se desploma para cualquiera con temblor, prisa o dedos grandes.
El papel semántico es el segundo. Un contenedor con modificador de pulsación se anuncia como un elemento genérico; un botón se anuncia como botón, y el lector de pantalla ofrece al usuario la acción correspondiente. Y un botón que solo contiene un icono necesita una descripción textual: sin ella, el anuncio es literalmente el silencio.
La capa de estado es el tercero, y el más frecuente entre quienes buscan una estética limpia. Desactivar la indicación visual de pulsación elimina la única confirmación de que el toque ha sido registrado, y con ella la retroalimentación que necesita quien navega con teclado o con un mando, porque esa misma capa es la que dibuja el foco.
El contraste del estado deshabilitado es el cuarto y el más silencioso: al construir a mano el conjunto de colores, se rellenan los estados normales y se dejan los deshabilitados en un valor cualquiera, produciendo texto ilegible justo en el momento en que el usuario necesita entender por qué no puede continuar.
flowchart TD
A[Quiero personalizar un componente] --> B{Que estoy cambiando}
B -->|Color forma o tipo| C[Nivel de tokens o valores por omision]
B -->|API o comportamiento externo| D[Envoltorio propio]
B -->|Estructura interna| E[Copia del codigo fuente]
C --> F[Garantias intactas]
D --> F
E --> G[Revisa area tactil papel semantico foco y estado deshabilitado]
style F fill:#a6e3a1,color:#11111b
style G fill:#f9e2af,color:#11111bLos mensajes emergentes de la biblioteca consultan al sistema cuánto tiempo deben permanecer visibles y alargan su duración cuando hay un lector de pantalla activo, porque un aviso que se lee en voz alta necesita más tiempo del que tarda un vistazo. Una implementación propia con un temporizador fijo hace desaparecer el mensaje a mitad de la locución.
Hay un modelo mental extendido y falso según el cual la accesibilidad es trabajo adicional que se incorpora al final, y de él se derivan todas las conversaciones improductivas sobre si hay presupuesto para ello. La realidad de esta plataforma es la inversa y conviene enunciarla sin suavizarla: la accesibilidad ya está hecha. Alguien pasó años calibrando opacidades de capas de estado, midiendo áreas de pulsación contra datos reales de precisión motriz, definiendo el vocabulario semántico que traduce una interfaz visual a una secuencia hablada, y ajustando tiempos de permanencia a la velocidad de una locución. Todo eso viene dentro de cada componente que instancias. Lo que ocurre después no es que el equipo “no añada” accesibilidad, es que la retira, pieza a pieza, con cada decisión que parece puramente estética: el icono que se hace pulsable a mano porque el botón traía demasiado espacio, la indicación que se desactiva porque el destello no gustaba en la revisión de diseño, el conjunto de colores construido desde cero porque era más limpio que copiar el de la biblioteca. Ninguna de esas decisiones se registra como una decisión sobre accesibilidad, y ahí está exactamente el problema: se toman en el vocabulario de la estética y se cobran en el de la exclusión. De esto se sigue el criterio que ordena toda esta lección y que no depende de recordar ninguna lista: cada vez que te apartes de un componente de la biblioteca, tienes la carga de la prueba. No basta con que el resultado se vea igual; hay que demostrar que sigue siendo pulsable con precisión baja, anunciable por voz, alcanzable por teclado, legible deshabilitado y con tiempos suficientes. Si no puedes demostrarlo, no has personalizado el componente: lo has degradado, y la única razón por la que nadie protesta es que las personas a las que acabas de dejar fuera no suelen escribir informes de error, simplemente desinstalan.
- Activa el lector de pantalla del sistema y recorre una pantalla entera con los ojos cerrados; anota cada elemento que se anuncia mal o no se anuncia.
- Conecta un teclado y navega con tabulación: localiza los controles que no reciben foco o que lo reciben sin indicación visible.
- Busca en tu código los modificadores de pulsación aplicados a iconos y mide su área táctil real.
- Encuentra un conjunto de colores construido a mano y comprueba el contraste de su estado deshabilitado.
- Elige la personalización más agresiva de tu proyecto y decide a qué peldaño de la escala pertenece y a cuál podría bajar.