wandres.dev
ACCESIBILIDAD · semántica y TalkBack

Etiquetar bien: descripción, rol y estado

Una etiqueta de accesibilidad no es un texto alternativo: es la definición del elemento para quien no lo ve. Esta lección separa las tres dimensiones que un nodo debe declarar y que casi todo el mundo mezcla —qué es, cómo se llama y en qué situación está—, estudia `contentDescription` frente a `stateDescription` y frente al rol, establece por qué el rol no debe escribirse en la descripción, desarrolla la retórica concreta de una etiqueta útil, y define con criterio operativo los dos casos en que un elemento debe volverse deliberadamente invisible para el árbol de semántica.

⏱ 21 min

Cuando un desarrollador escribe su primera descripción de accesibilidad casi siempre produce la misma frase: algo como icono de papelera para borrar el elemento seleccionado. Esa frase contiene, comprimidos y en desorden, tres tipos de información que el sistema espera recibir por canales distintos, y por eso el resultado suena mal, se repite y a veces miente. La accesibilidad de un elemento no es un texto: es una tupla. Qué es —su rol—, cómo se llama —su nombre—, en qué situación está —su estado— y qué se le puede hacer —sus acciones—. TalkBack compone el enunciado final a partir de esas piezas, en el idioma del usuario, con las convenciones de la plataforma y respetando su verbosidad configurada. Tu trabajo no es redactar la locución, sino rellenar los campos correctos. Confundir los campos es la causa del noventa por ciento de las etiquetas que técnicamente existen y en la práctica estorban.

🎯 Al terminar esta lección sabrás
  • Separar con rigor las cuatro dimensiones de un nodo accesible: rol, nombre, estado y acciones.
  • Usar contentDescription, stateDescription y Role en el campo que corresponde a cada uno.
  • Redactar etiquetas que describan la intención del elemento y no su apariencia gráfica.
  • Decidir con criterio cuándo un elemento debe desaparecer del árbol y con qué mecanismo hacerlo.

Cuatro campos, no una frase

La plataforma modela cada nodo accesible con dimensiones ortogonales, y los servicios de asistencia las verbalizan en un orden y con un formato que el usuario controla. Alguien con TalkBack en verbosidad baja quizá solo oiga el nombre; en verbosidad alta oirá el rol y las acciones disponibles. Si tú metes el rol dentro del nombre, ese usuario pierde el control: oirá boton dos veces o lo oirá cuando pidió no oírlo.

flowchart LR
A[Nodo semantico] --> B[Rol: que es]
A --> C[Nombre: como se llama]
A --> D[Estado: como esta ahora]
A --> E[Acciones: que puede hacerse]
B --> F[Enunciado compuesto por el servicio]
C --> F
D --> F
E --> F

En Compose los cuatro campos tienen su sitio. El rol se declara con la propiedad role dentro de semantics, o llega implícito a través de modificadores como toggleable y selectable y de los componentes de Material. El nombre es contentDescription para elementos no textuales y el propio texto para los textuales. El estado es stateDescription, y las acciones se declaran con onClick, onLongClick o con las acciones personalizadas del propio bloque semántico.

El catálogo de roles disponibles es corto y deliberadamente cerrado: botón, casilla de verificación, interruptor, opción de radio, imagen, pestaña, barra desplazadora y poco más. Esa brevedad desconcierta a quien viene de la web, donde el vocabulario de roles es enorme, pero responde a una lógica sólida: el rol solo sirve si el usuario tiene una expectativa consolidada sobre cómo se comporta ese tipo de control. Inventar roles que nadie conoce no comunica nada. Cuando tu elemento no encaja en ninguno de los existentes, la respuesta correcta no es forzar el más parecido sino describir su comportamiento mediante estado y acciones bien nombradas.

Una precisión importante sobre el nombre de los elementos textuales: un Text no necesita contentDescription, porque su propio contenido ya alimenta la propiedad de texto del nodo. Añadírsela es duplicar información y, en el peor caso, contradecirla. La descripción está reservada para lo que no tiene texto: iconos, imágenes, ilustraciones, superficies dibujadas a mano.

IconButton(
    onClick = { alternarFavorito() },
    modifier = Modifier.semantics {
        role = Role.Switch
        stateDescription = if (esFavorito) "Marcado" else "Sin marcar"
    },
) {
    Icon(
        imageVector = if (esFavorito) Icons.Filled.Star else Icons.Outlined.Star,
        contentDescription = "Favorito",
    )
}

Fíjate en lo que no dice esa descripción. No dice estrella, porque la estrella es la implementación gráfica de la idea y podría cambiar por un corazón la semana que viene sin que cambie el significado. No dice marcado, porque eso es estado y va en su campo. Y no dice botón, porque eso es rol. Lo único que queda en el nombre es la identidad estable del control.

La retórica de una buena etiqueta

Una vez separados los campos, redactar el nombre se vuelve un problema acotado y con reglas verificables.

La primera regla es describir la función, no el dibujo. La pregunta correcta no es qué se ve aquí sino qué ocurre si activo esto. Un icono de tres puntos verticales no es tres puntos: es más opciones. Una lupa no es una lupa: es buscar. La traducción del glifo al propósito es exactamente el trabajo cognitivo que el usuario vidente hace en un instante y que el usuario de lector de pantalla no puede hacer.

La segunda regla es que la etiqueta debe ser específica en su contexto. Diez botones llamados igual en una lista son diez botones inútiles: el usuario los recorre en secuencia sin saber a qué elemento pertenecen. Si la lista muestra conversaciones, la etiqueta debe incorporar el discriminante.

IconButton(
    onClick = { archivar(conversacion.id) },
    modifier = Modifier.semantics {
        contentDescription = "Archivar conversacion con ${conversacion.remitente}"
    },
) { Icon(Icons.Default.Archive, contentDescription = null) }

La tercera regla es la brevedad disciplinada. La etiqueta se escucha en serie, no se ojea, y cada palabra superflua se paga en tiempo por cada visita al elemento. Suprime artículos innecesarios, adjetivos decorativos y todo lo que el rol ya comunica.

La cuarta regla, menos evidente, es la estabilidad. Una etiqueta que cambia de redacción entre pantallas para referirse a la misma acción obliga al usuario a reaprender el vocabulario de tu aplicación en cada contexto. Si en la lista dice archivar y en el detalle dice mover a archivados, son dos conceptos distintos para quien no ve que el icono es idéntico. Un glosario compartido de verbos y sustantivos —el mismo que debería regir los textos visibles— resuelve el problema de raíz y suele ser el primer artefacto que produce un equipo que se toma esto en serio.

La quinta y última es la localización. Las descripciones son texto de interfaz como cualquier otro y deben salir del sistema de recursos, no de literales incrustados en el código. Escribirlas a mano en el punto de uso es la vía más rápida para que una aplicación traducida a doce idiomas hable español a la mitad de sus usuarios en cuanto enciendan el lector de pantalla.

Icon(
    imageVector = Icons.Default.Archive,
    contentDescription = stringResource(R.string.accion_archivar),
)
⚠️
Los tres vicios que arruinan una etiqueta correcta

El primero es la redundancia de rol: escribir boton, enlace o casilla dentro de la descripción, con lo que el usuario lo oye dos veces. El segundo es el estado congelado: poner activado en el nombre en lugar de en el estado, con lo que la etiqueta miente en cuanto el usuario lo desactiva. El tercero es la fuga de implementación: describir el recurso gráfico —icono de flecha hacia abajo— en lugar de la acción —desplegar detalles—. Los tres producen etiquetas que pasan cualquier auditoría automática y que ningún usuario real agradece.

El estado es dinámico y casi nunca se declara

De las cuatro dimensiones, el estado es la que más se olvida, y su ausencia produce un fallo particularmente cruel: el usuario activa un control, no recibe confirmación alguna y no sabe si su gesto tuvo efecto.

stateDescription sustituye la verbalización que el sistema haría por defecto para estados conocidos y añade verbalización donde no la había. Un interruptor de Material ya anuncia activado o desactivado sin ayuda; lo que no puede saber es que en tu dominio ese interruptor significa recibir notificaciones o silenciado hasta mañana.

Estados que debes declarar

Expandido y contraído, seleccionado dentro de un grupo, cargando, con error de validación, disponible o agotado, sincronizado o pendiente. Todo lo que el usuario vidente deduce de un color, un tamaño o una posición.

🔁

Estados que ya vienen dados

El activado de toggleable, el seleccionado de selectable, el habilitado o deshabilitado de los controles de Material. Redeclararlos duplica la locución sin añadir información.

Hay un caso particular de estado que merece mención aparte porque tiene propiedad propia: los valores dentro de un rango. Un control deslizante, una barra de progreso o un medidor no se describen bien con una cadena, porque el usuario necesita conocer el valor actual y sus extremos para situarse. Para eso existen progressBarRangeInfo y las acciones de ajuste, que permiten al servicio anunciar la posición relativa y modificarla con gestos de incremento.

Modifier.semantics {
    progressBarRangeInfo = ProgressBarRangeInfo(
        current = paginaActual.toFloat(),
        range = 1f..totalPaginas.toFloat(),
        steps = totalPaginas - 2,
    )
}

Junto al estado conviene mencionar su pariente temporal: los cambios que ocurren sin que el usuario los provoque. Cuando el estado cambia por una respuesta de red o por un temporizador, el árbol se actualiza pero nadie lo anuncia. Para eso existen los anuncios de región dinámica, que se declaran con liveRegion en el nodo que cambia, y que instruyen al servicio para que interrumpa —modo asertivo— o espere su turno —modo educado— al leer la novedad. Un liveRegion mal puesto en un elemento que cambia cada segundo convierte la aplicación en un martilleo insoportable, así que la propiedad se reserva para transiciones significativas y poco frecuentes.

Cuándo un elemento debe ser invisible

La accesibilidad no consiste en describirlo todo. Un árbol saturado de nodos irrelevantes es tan hostil como uno vacío, porque obliga a atravesar ruido para llegar al contenido. Hay dos situaciones en las que la respuesta correcta es retirar el elemento del árbol.

La primera es la decoración pura: un separador, un degradado de fondo, un icono que acompaña a un texto que ya dice lo mismo. Para esos casos, contentDescription = null en un Icon o Image marca el elemento como no informativo y lo excluye del recorrido. No es una omisión perezosa: es una declaración explícita de que ese elemento no aporta información, y por eso la API obliga a escribirla.

La segunda es la redundancia por agrupación: cuando un contenedor ya describe el conjunto, sus hijos individuales no deben ser paradas independientes. Ese caso se resuelve con fusión, que es el tema de la lección siguiente, o en última instancia con clearAndSetSemantics cuando necesitas reemplazar por completo la semántica de un subárbol por una descripción propia.

Row(
    modifier = Modifier.clearAndSetSemantics {
        contentDescription = "Valoracion media 4 coma 2 sobre 5"
    },
) {
    repeat(5) { i -> Icon(estrellaPara(i), contentDescription = null) }
    Text("4,2")
}

La advertencia sobre clearAndSetSemantics merece repetirse: borra también las acciones de los descendientes. Si dentro del subárbol hay algo pulsable, ese algo deja de ser alcanzable. Úsalo únicamente sobre composiciones puramente presentacionales.

Etiquetar es traducir una convención visual a lenguaje

Conviene entender qué operación intelectual estás haciendo realmente cuando escribes una etiqueta, porque no es documentación y no es traducción literal. Toda interfaz gráfica descansa sobre un enorme cuerpo de convenciones aprendidas: un icono de disquete significa guardar para una generación que nunca vio un disquete, tres líneas horizontales significan menú por acuerdo tácito, el gris significa deshabilitado, la posición superior derecha significa acción secundaria, el subrayado significa enlace. Ninguna de esas equivalencias es natural; todas se aprendieron por exposición. El usuario vidente que atraviesa tu pantalla no está leyendo, está reconociendo patrones, y lo hace tan rápido que confunde el reconocimiento con la percepción. Cuando escribes una etiqueta, lo que haces es descomprimir esa convención y devolverla a la forma explícita de la que salió. Por eso la etiqueta correcta casi nunca describe lo que se ve: describe lo que se entiende. Y por eso el ejercicio tiene un efecto secundario que a menudo sorprende a quien lo hace por primera vez con seriedad: cuando una etiqueta resulta imposible de escribir sin sonar absurda, el problema casi siempre está en el diseño, no en la redacción. Un control cuya función no cabe en cinco palabras es un control que el usuario vidente tampoco entiende, solo que él lo disimula pulsándolo para ver qué pasa. Etiquetar bien es, entre otras cosas, el detector de humo más barato que existe para la ambigüedad de una interfaz.

⚔️ Reescribe tus etiquetas desde cero
  1. Extrae todas las cadenas de contentDescription de una pantalla y clasifícalas en las cuatro dimensiones: cuántas contienen rol, cuántas estado, cuántas describen el dibujo.
  2. Reescribe las que mezclen dimensiones, moviendo cada pieza a su campo correcto, y comprueba con TalkBack que no se repite nada.
  3. Busca un control cuyo estado cambia y que nunca lo anuncia. Añade stateDescription con vocabulario de tu dominio, no genérico.
  4. Localiza tres iconos decorativos que sigan siendo paradas del recorrido y márcalos como no informativos. Cuenta los deslizamientos ahorrados.
  5. Elige la etiqueta que más te costó redactar y pregúntate si el problema era la etiqueta o el control. Propón un cambio de diseño si lo era.