En 04-01 vimos que los controles nativos de formulario (<input>, <select>, <textarea>, <button>) traen rol, foco y operación por teclado de fábrica. Pero un formulario accesible necesita algo más que buenos controles: necesita que cada control tenga nombre, que los campos relacionados estén agrupados, que las ayudas y los errores lleguen a todo el mundo y que el orden de foco sea lógico. Esos son precisamente los criterios 3.3.x ("Ayuda a la entrada de datos") que en el Módulo 3 estudiamos como requisito y que ahora implementamos.
El terreno de práctica es el formulario de matrícula/registro de Cursalia: nombre, correo, teléfono, contraseña, nivel de estudios, intereses y aceptación de condiciones. Lo iremos construyendo pieza a pieza, siempre con ejemplos correctos frente a incorrectos, pensando en Marta (NVDA debe leerle la etiqueta de cada campo), Hugo (necesita instrucciones claras y sin ambigüedad) y Lucía (lo recorre entero con el teclado).
Contenido
- La etiqueta: asociar
labely control - Por qué el placeholder no es una etiqueta
- Agrupar con
fieldsetylegend - Campos obligatorios:
requiredyaria-required - Instrucciones y ayudas con
aria-describedby - Errores accesibles (y un primer vistazo a
role="alert") autocompletey tipos deinput- Foco y orden lógico
- Errores comunes y consejos
- Ejercicios
- Conclusión
La etiqueta: asociar label y control
Todo control de formulario debe tener una etiqueta accesible. La etiqueta es el name del control (recuerda 4.1.2): sin ella, Marta oye "cuadro de edición" sin saber qué escribir. Hay dos formas correctas de asociar un <label> con su control.
Opción A — for / id (la más común y flexible): el atributo for del label apunta al id del control.
<!-- CORRECTO: for apunta al id -->
<label for="email">Correo electrónico</label>
<input type="email" id="email" name="email">Opción B — label envolvente: el control va dentro del <label>. No hace falta for/id.
<!-- CORRECTO: label envuelve al control -->
<label>
Correo electrónico
<input type="email" name="email">
</label>Ambas hacen que al pulsar sobre el texto se enfoque el campo (área de clic mayor, algo que agradece Lucía) y que el lector lo anuncie con su nombre. El error habitual es no asociarlos: texto suelto al lado de un input.
<!-- INCORRECTO: el texto no está asociado al campo -->
<span>Correo electrónico</span>
<input type="email" name="email">Aquí no hay <label> (o su for no coincide con ningún id): Marta oye "cuadro de edición en blanco". Si por diseño no puedes mostrar un texto visible (por ejemplo, el buscador del catálogo con solo una lupa), usa aria-label, que veremos en 04-03:
Por qué el placeholder no es una etiqueta
Es tentador poner solo un placeholder="Correo electrónico" y ahorrarse el <label>. Es un error serio por varios motivos:
| Problema del placeholder como etiqueta | A quién afecta |
|---|---|
| Desaparece al empezar a escribir; ya no sabes qué era ese campo | Hugo (memoria/atención), todos |
| Su contraste suele ser bajo (gris claro) y no cumple 1.4.3 | Sofía (baja visión) |
| Muchos lectores no lo anuncian como nombre fiable | Marta |
| No se puede pulsar para enfocar el campo | Lucía |
<!-- INCORRECTO: solo placeholder, sin label -->
<input type="email" placeholder="Correo electrónico">
<!-- CORRECTO: label visible; el placeholder, si acaso, solo como ejemplo de formato -->
<label for="email">Correo electrónico</label>
<input type="email" id="email" placeholder="[email protected]">Regla: el placeholder es, como mucho, una pista de formato; nunca sustituye a la etiqueta.
Agrupar con fieldset y legend
Cuando varios controles forman un solo grupo conceptual —un conjunto de radios (elige uno) o de checkboxes (elige varios)—, hay que envolverlos en <fieldset> y darles un título común con <legend>. Así el lector anuncia el grupo ("Nivel de estudios, grupo") antes de cada opción, dando el contexto que un radio suelto no tendría.
<!-- CORRECTO: grupo de radios con fieldset/legend -->
<fieldset>
<legend>Nivel de estudios</legend>
<label><input type="radio" name="nivel" value="secundaria"> Secundaria</label>
<label><input type="radio" name="nivel" value="grado"> Grado universitario</label>
<label><input type="radio" name="nivel" value="master"> Máster o superior</label>
</fieldset>Marta oye: "Nivel de estudios, grupo. Secundaria, botón de radio, 1 de 3". Sin el fieldset, oiría tres radios sueltos sin saber a qué pregunta responden.
<!-- INCORRECTO: un texto suelto no agrupa nada semánticamente -->
<p>Nivel de estudios</p>
<label><input type="radio" name="nivel" value="grado"> Grado</label>Lo mismo aplica a los intereses (checkboxes) o a la aceptación de varias condiciones. No abuses del fieldset para campos independientes (nombre, correo): es solo para grupos.
Campos obligatorios: required y aria-required
Marcar un campo como obligatorio tiene dos caras que hay que cubrir a la vez: la funcional y la comunicativa.
required: atributo nativo. Activa la validación del navegador y expone el estado "requerido" al lector (Marta oye "requerido"). Es la primera opción.aria-required="true": solo si necesitas comunicar la obligatoriedad sin activar la validación nativa del navegador. No dupliques ambos sin motivo; conrequirednormalmente basta.- Indicación visual y textual: no basta con un asterisco rojo (falla 1.4.1, "no solo por color/forma"). Explica qué significa el asterisco y, mejor aún, refuérzalo con texto.
<!-- CORRECTO: obligatorio comunicado de tres formas: nativa, visual y textual -->
<p id="leyenda-oblig">Los campos marcados con * son obligatorios.</p>
<label for="nombre">Nombre completo *</label>
<input type="text" id="nombre" name="nombre" required
aria-describedby="leyenda-oblig"><!-- INCORRECTO: la obligatoriedad vive solo en el color del asterisco -->
<label for="nombre">Nombre completo <span style="color:red">*</span></label>
<input type="text" id="nombre"> <!-- ni required, ni explicación del * -->Instrucciones y ayudas con aria-describedby
Muchos campos necesitan una ayuda: formato de la contraseña, para qué se usa el teléfono, etc. Para que esa ayuda sea leída junto al campo (y no quede como texto huérfano al lado), se asocia con aria-describedby, que apunta al id del texto de ayuda. El lector lee primero la etiqueta y después la descripción.
<!-- CORRECTO: la ayuda se asocia con aria-describedby -->
<label for="pass">Contraseña</label>
<input type="password" id="pass" name="pass"
aria-describedby="ayuda-pass" required>
<p id="ayuda-pass">Mínimo 8 caracteres, con una letra y un número.</p>Marta oye: "Contraseña, cuadro de edición, requerido. Mínimo 8 caracteres, con una letra y un número". Hugo lee esa misma instrucción clara antes de escribir, no después de fallar. aria-describedby puede apuntar a varios ids separados por espacios (por ejemplo, la ayuda y el mensaje de error del mismo campo).
<!-- INCORRECTO: la ayuda existe visualmente pero no está asociada -->
<label for="pass">Contraseña</label>
<input type="password" id="pass">
<p>Mínimo 8 caracteres, con una letra y un número.</p> <!-- sin aria-describedby: el lector no la vincula -->Errores accesibles (y un primer vistazo a role="alert")
Cuando el usuario se equivoca, el criterio 3.3.1 exige identificar el error en texto (no solo con color) y describirlo de forma útil. Buenas prácticas:
- Texto explícito, no solo un borde rojo: "El correo no es válido: falta la @".
- Asociado al campo con
aria-describedby, para que el lector lo lea al enfocar ese campo. - No depender del color: añade un icono o la palabra "Error" (esto es 1.4.1, que veremos a fondo en 04-04).
- Mover el foco al primer campo con error o resumir los errores al principio ayuda a Lucía y a Marta.
<!-- CORRECTO: error en texto, asociado, y no solo por color -->
<label for="email">Correo electrónico *</label>
<input type="email" id="email" name="email" required
aria-describedby="err-email" aria-invalid="true">
<p id="err-email" class="error">
<span aria-hidden="true">⚠️</span> Error: el correo debe incluir una @.
</p>Aquí aria-invalid="true" marca el campo como inválido en el árbol de accesibilidad, y aria-describedby hace que el mensaje se lea al enfocarlo.
Cuando el error aparece de forma dinámica (tras pulsar "Enviar", sin recargar la página), hace falta además anunciarlo aunque el foco esté en otro sitio. Para eso existen las live regions. Una introducción breve: un contenedor con role="alert" (o aria-live="assertive") hace que el lector lea automáticamente el texto que aparezca dentro, sin que el usuario tenga que ir a buscarlo.
<!-- El lector anunciará el texto en cuanto tu JS lo inserte aquí -->
<div role="alert" id="resumen-errores"></div>// Al validar, insertas el mensaje y NVDA lo lee solo:
document.getElementById('resumen-errores').textContent =
'Revisa el formulario: hay 2 campos con errores.';Nos quedamos en esta introducción: el detalle de aria-live, role="status" vs role="alert" y las regiones dinámicas se ve a nivel de marcado en 04-03, y la validación completa con JavaScript (cuándo validar, cómo mover el foco, actualizar mensajes en vivo) pertenece al Módulo 5.
autocomplete y tipos de input
Elegir bien el type y añadir autocomplete reduce el esfuerzo de todos, y de forma muy notable para Lucía (menos tecleo), Hugo (menos que recordar) y quien usa control por voz o móvil.
| Campo | type correcto |
autocomplete |
Beneficio |
|---|---|---|---|
| Correo | email |
email |
Teclado con @, validación de formato, autorrelleno |
| Teléfono | tel |
tel |
Teclado numérico en móvil |
| Nombre | text |
name |
Autorrelleno del nombre guardado |
| Contraseña (registro) | password |
new-password |
El gestor sugiere una segura |
<!-- CORRECTO: tipos y autocomplete que ayudan al navegador y a la persona -->
<label for="email">Correo electrónico</label>
<input type="email" id="email" name="email" autocomplete="email">
<label for="tel">Teléfono</label>
<input type="tel" id="tel" name="tel" autocomplete="tel">autocomplete cumple además el criterio 1.3.5 ("Identificar el propósito de la entrada"), que ayuda a herramientas que adaptan o rellenan campos por el usuario.
Foco y orden lógico
El orden en que Lucía llega a los campos al pulsar Tab debe seguir el orden visual y lógico del formulario. La clave es que el orden del DOM coincida con el orden visual: si maquetas de arriba abajo en el HTML, el foco irá bien sin trucos.
- No uses
tabindexpositivo (tabindex="1","2"...): rompe el orden natural y es una fuente constante de bugs. Deja que el orden lo dicte el DOM. - Un
<button type="submit">real permite enviar el formulario con Enter desde cualquier campo. - Tras enviar con errores, llevar el foco al resumen de errores o al primer campo fallido ahorra a Lucía tabular a ciegas (implementación con JS: Módulo 5).
<!-- INCORRECTO: tabindex positivo fuerza un orden artificial y frágil -->
<input id="a" tabindex="3">
<input id="b" tabindex="1">
<input id="c" tabindex="2"><!-- CORRECTO: sin tabindex; el orden del DOM ya es el orden de foco -->
<input id="a">
<input id="b">
<input id="c">Errores Comunes y Consejos
- Campos sin
<label>asociado. Es el fallo número uno. Cada control necesita nombre porfor/id, label envolvente o, en último caso,aria-label. - Usar el placeholder como etiqueta. Desaparece, contrasta mal y no es fiable como nombre. Pon siempre etiqueta visible.
- Obligatoriedad solo por color. El asterisco rojo no basta: añade
requiredy explica qué significa el*. - Ayudas y errores sin asociar. Un texto de ayuda al lado del campo que no está enlazado con
aria-describedbyes invisible para el lector. - Errores solo con borde rojo. Marta no ve el color: el error debe ir en texto y estar asociado.
- Radios/checkboxes sin
fieldset/legend. Pierden el contexto de la pregunta. tabindexpositivo. Casi nunca es la solución; ordena el DOM.- Consejo: rellena tu formulario entero solo con el teclado y con un lector, con los ojos cerrados. Si no sabes qué pide un campo o dónde está el error, aún no es accesible.
Ejercicios
Ejercicio 1: dar nombre a los campos
Este fragmento del registro de Cursalia no asocia etiquetas. Corrígelo usando for/id y sustituye el placeholder-etiqueta por una etiqueta real.
Ejercicio 2: agrupar los intereses
Los usuarios pueden marcar varios intereses. Marca correctamente este grupo de checkboxes para que el lector anuncie el título del grupo.
<p>Intereses</p>
<label><input type="checkbox" name="int" value="dev"> Desarrollo</label>
<label><input type="checkbox" name="int" value="ux"> Diseño UX</label>
<label><input type="checkbox" name="int" value="data"> Datos</label>Ejercicio 3: un campo con ayuda y error accesibles
Escribe el campo "Contraseña": etiqueta visible, obligatorio, con una ayuda de formato asociada y un mensaje de error (asociado y no dependiente solo del color) para cuando la contraseña sea demasiado corta.
Soluciones
Solución 1. Cada control recibe una etiqueta asociada; el placeholder deja paso a un <label> real:
<label for="nombre">Nombre</label>
<input type="text" id="nombre" name="nombre" autocomplete="name">
<label for="email">Correo electrónico</label>
<input type="email" id="email" name="email" autocomplete="email">Solución 2. El grupo entero va en un <fieldset> con <legend>:
<fieldset>
<legend>Intereses</legend>
<label><input type="checkbox" name="int" value="dev"> Desarrollo</label>
<label><input type="checkbox" name="int" value="ux"> Diseño UX</label>
<label><input type="checkbox" name="int" value="data"> Datos</label>
</fieldset>Ahora el lector anuncia "Intereses, grupo" antes de cada casilla.
Solución 3. Etiqueta visible + required + ayuda con aria-describedby + error en texto asociado, con icono además del color:
<label for="pass">Contraseña *</label>
<input type="password" id="pass" name="pass" required
autocomplete="new-password"
aria-describedby="ayuda-pass err-pass"
aria-invalid="true">
<p id="ayuda-pass">Mínimo 8 caracteres, con una letra y un número.</p>
<p id="err-pass" class="error">
<span aria-hidden="true">⚠️</span> Error: la contraseña es demasiado corta.
</p>Fíjate en que aria-describedby apunta a dos ids (ayuda y error), así el lector lee ambos. Cuando la contraseña sea válida, tu código quitará aria-invalid y el mensaje de error (eso ya es lógica de JS, Módulo 5).
Conclusión
Un formulario accesible no es magia: es etiquetar cada control, agrupar lo que va junto, comunicar la obligatoriedad y las ayudas por texto asociado, describir los errores de forma clara y no solo por color, y respetar el orden de foco del DOM. Con esto, el formulario de matrícula de Cursalia deja de ser un muro para Marta, Hugo y Lucía. Hemos apoyado casi todo en HTML nativo y solo hemos asomado la cabeza a aria-describedby, aria-invalid y role="alert".
Esos atributos aria-* son la puerta a la siguiente lección. En 04-03 · Uso de Roles y Propiedades ARIA daremos un paso atrás para entender qué es ARIA, sus cinco reglas de oro (empezando por "no lo uses si hay HTML nativo"), cómo se calcula el nombre accesible que hemos ido usando (aria-label, aria-labelledby, aria-describedby) y cómo describir estados como aria-expanded en el desplegable de filtros de Cursalia, siempre a nivel de marcado.
Curso de Accesibilidad Web
Módulo 1: Introducción a la Accesibilidad Web
- ¿Qué es la Accesibilidad Web?
- Importancia de la Accesibilidad Web
- Visión General de las Leyes y Normas de Accesibilidad
- Introducción a WCAG
Módulo 2: Comprensión de Discapacidades y Tecnologías de Asistencia
- Tipos de Discapacidades
- Visión General de Tecnologías de Asistencia
- Cómo las Personas con Discapacidades Usan la Web
Módulo 3: Principios del Diseño Accesible
- Perceptible: Hacer el Contenido Disponible a los Sentidos
- Operable: Interfaz de Usuario y Navegación
- Comprensible: Información y Operación
- Robusto: Compatibilidad con Tecnologías Actuales y Futuras
Módulo 4: Implementación de Accesibilidad en HTML y CSS
- HTML Semántico
- Formularios Accesibles
- Uso de Roles y Propiedades ARIA
- Contraste de Color y Redimensionamiento de Texto
Módulo 5: Accesibilidad en JavaScript y Multimedia
- Creación de Widgets de JavaScript Accesibles
- Accesibilidad del Teclado
- Contenido de Video y Audio Accesible
- Proporcionar Alternativas de Texto para Imágenes
Módulo 6: Pruebas y Evaluación de la Accesibilidad
- Técnicas de Pruebas Manuales
- Herramientas de Pruebas Automatizadas
- Pruebas de Usuario con Tecnologías de Asistencia
- Interpretación de Informes de Accesibilidad
