La accesibilidad no es un sello, ni un widget, ni un puntaje perfecto de auditoría. Es si alguien puede entender la página y terminar la tarea con los medios de entrada y salida que usa. Empieza con HTML nativo, mantén intacto el camino del teclado y comprueba el resultado real.
🎙️ Publicado y grabado: ·
Los navegadores ya saben qué significa un botón, un encabezado, un enlace, una lista, una tabla o un campo de formulario. Los elementos nativos traen juntos el comportamiento de teclado, los roles, los estados y el soporte de la plataforma. Un div clicable no trae nada de eso. Mi regla es directa: si un elemento HTML hace el trabajo, úsalo. Agrega ARIA solo para información que el HTML no puede expresar.
<!-- Wrong: no role, no keyboard action, no button semantics -->
<div class="save" onclick="save()">Save</div>
<!-- Right: Enter and Space work without custom key code -->
<button type="button" onclick="save()">Save</button>
<main>
<h1>Account settings</h1>
<nav aria-label="Settings">...</nav>
</main>
role="button" en un div te sigue dejando a cargo del foco, de Enter, de la barra espaciadora, del comportamiento deshabilitado y del estado. Eso es código frágil reemplazando a un elemento nativo. No uses ARIA para maquillar el árbol de accesibilidad mientras la interacción sigue rota.Desconecta el mouse. Tab debería recorrer los controles interactivos en un orden razonable. Shift más Tab debería moverse hacia atrás. Enter debería activar enlaces y botones; la barra espaciadora debería activar botones. Escape debería cerrar una capa temporal cuando se espera que se cierre. Las flechas pertenecen dentro de widgets compuestos específicos, no como reemplazo de Tab en toda la página.
/* Keep DOM order equal to reading and focus order */
<a href="#main" class="skip">Skip to main content</a>
...
<main id="main" tabindex="-1">...</main>
.skip { position: absolute; left: -9999px; }
.skip:focus { left: 1rem; top: 1rem; z-index: 10; }
/* Never do this globally */
*:focus { outline: none; }
Evita los valores positivos de tabindex. Valores como uno o dos crean un segundo sistema de orden que se desvía del DOM. Usa controles nativos enfocables y el orden del DOM. Usa tabindex="-1" solo cuando un script deba mover el foco a algo que no debería entrar en la navegación normal con Tab.
El foco del teclado responde una sola pregunta: ¿dónde va a ocurrir mi próxima acción? Dale un indicador visible que sobreviva sobre fondos claros y oscuros. Cuando se abre un diálogo, mueve el foco dentro; mantén Tab dentro del diálogo; ciérralo con Escape; después devuelve el foco al control que lo abrió. Cuando cambia contenido ordinario, no muevas el foco solo para anunciar que pasó algo.
:focus-visible {
outline: 3px solid #0b63ce;
outline-offset: 3px;
}
const opener = document.activeElement;
dialog.showModal();
dialog.querySelector("button, input").focus();
dialog.addEventListener("close", () => opener?.focus());
[aria-hidden="true"] elements contain focusable descendants. Uno: reprodúcelo tabulando mientras el panel lateral o el modal está cerrado. Dos: identifica el enlace, botón o campo que sigue recibiendo foco. Tres: usa el atributo nativo hidden, usa inert o quita del DOM el panel cerrado. Cuatro: vuelve a abrirlo y restaura el foco de forma deliberada. Cinco: prueba de nuevo el orden de Tab hacia adelante y hacia atrás. Agregar más aria-hidden no quita el foco de teclado.Un lector de pantalla no narra tu maquetación CSS. Usa el árbol de accesibilidad que se produce a partir del HTML, el texto, las etiquetas y ARIA. Cada control necesita un nombre accesible útil. Su rol debe corresponder a la acción. Su estado, como expandido, marcado, seleccionado o inválido, tiene que actualizarse cuando la interfaz cambia. Los encabezados y los landmarks permiten saltar en lugar de escuchar desde el principio.
<button aria-expanded="false" aria-controls="filters">
Filters
</button>
<div id="filters" hidden>...</div>
/* On open, change both the DOM and exposed state */
button.setAttribute("aria-expanded", "true");
filters.hidden = false;
/* Icon-only button needs a name */
<button aria-label="Close dialog">×</button>
Prueba con al menos una pareja real de navegador y lector de pantalla que use tu audiencia. En Windows, NVDA con Firefox o Chrome es un buen punto de partida. En dispositivos Apple, usa VoiceOver con Safari. Aprende primero una ruta corta: encabezados, landmarks, controles de formulario, enlaces y botones. Leer cada oración de forma lineal no es como navegan las personas con experiencia.
El texto de placeholder es una pista, no una etiqueta. Conecta una etiqueta visible a cada campo. Pon las instrucciones antes de que se necesiten. Al enviar, muestra un resumen breve de errores, enlaza cada mensaje con su campo, marca los campos inválidos y conserva todo lo que la persona ya escribió. El texto de error debe decir qué pasó y cómo arreglarlo.
<label for="email">Work email</label>
<input id="email" name="email" type="email"
autocomplete="email"
aria-describedby="email-hint email-error">
<p id="email-hint">Use the address for your company account.</p>
<p id="email-error">Enter an address such as [email protected].</p>
/* Set only after validation fails */
input.setAttribute("aria-invalid", "true");
An invalid form control with name='email' is not focusable. Uno: envía el formulario y confirma que el campo obligatorio está dentro de un paso oculto o de un panel colapsado. Dos: no mantengas ocultos controles con required nativo mientras el navegador valida. Tres: muestra el paso antes de validar, o deshabilita y quita del envío los controles inactivos. Cuatro: pon el foco en el primer campo inválido y expón su mensaje. Cinco: envía otra vez usando solo el teclado. No silencies la consola quitando la validación sin poner nada en su lugar.WCAG usa ratios de contraste, no opiniones sobre si un gris se ve legible. El texto normal generalmente necesita cuatro punto cinco a uno. El texto grande generalmente necesita tres a uno. Los bordes de controles importantes y los indicadores de foco también necesitan suficiente contraste. Mide el primer plano real sobre el fondo real, incluyendo hover, deshabilitado, seleccionado, error, modo claro y modo oscuro.
/* Information survives without color */
.error {
color: #9b1c1c;
border-left: 4px solid currentColor;
}
.error::before { content: "Error: "; font-weight: 700; }
/* Keep links identifiable in body copy */
.prose a { color: #075db7; text-decoration: underline; }
.prose a:hover { text-decoration-thickness: 3px; }
El texto alternativo debe hacer el trabajo que hace la imagen en su contexto. Un logo enlazado puede nombrarse por su destino. Un gráfico necesita la conclusión y sus datos cerca. Un adorno decorativo debe llevar alt vacío para que se omita. No empieces con «imagen de». El lector de pantalla ya anuncia que es una imagen.
<!-- Informative -->
<img src="checkout-error.png"
alt="Checkout shows Card declined below the card number">
<!-- Decorative -->
<img src="wave.svg" alt="">
<!-- Video: provide accurate captions and a transcript -->
<video controls>
<source src="setup.mp4" type="video/mp4">
<track kind="captions" srclang="en" src="setup-en.vtt"
label="English" default>
</video>
Los subtítulos automáticos son un borrador. Corrige nombres, términos técnicos, puntuación, cambios de hablante y sonidos con significado. El material solo de audio necesita transcripción. Un video que transmite acción visual esencial puede necesitar audiodescripción o una versión narrada equivalente.
Las interfaces de una sola página cambian sin cargar un documento nuevo. Los lectores de pantalla pueden no notarlo. Usa una live region para un estado corto que deba anunciarse, como «Guardado» o «Cinco resultados». Deja la live region en el DOM inicial y luego cambia su texto. No pongas una lista completa de resultados en una live region assertive.
<p id="status" role="status" aria-live="polite"></p>
async function save() {
status.textContent = "Saving";
await saveSettings();
status.textContent = "Settings saved";
}
/* Respect a user's motion preference */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto; }
.decorative-animation { animation: none; }
}
Cargando, éxito y fallo son estados, no decoración visual. Deshabilita el envío duplicado solo cuando haga falta, expón el estado de ocupado y dale a la persona una vía para reintentar. Si la navegación entre rutas cambia el encabezado principal, actualiza el título del documento y coloca el foco donde empieza la página nueva.
WCAG organiza sus requisitos alrededor de que el contenido sea perceptible, operable, comprensible y robusto. Para la mayoría del trabajo de producto, apunta al nivel WCAG que exija tu política o la ley, normalmente nivel A y doble A. Pero una checklist no puede decirte si el checkout se entiende, si el foco cae en un lugar útil o si el texto alternativo comunica lo importante.
Fast test order:
1. Zoom to 200% and check reflow
2. Finish the main task with keyboard only
3. Inspect headings, landmarks, names, roles, and states
4. Run automated checks and fix definite failures
5. Use a screen reader on the main task and error path
6. Test high contrast, dark mode, and reduced motion
7. Include disabled users in research before release
Prueba el recorrido de usuario completo más pequeño, no una galería de componentes aislados. Incluye el camino feliz, el fallo de validación, el estado de carga, el estado vacío y la recuperación de un error del servidor.
Structure
[ ] One descriptive h1; headings do not skip for styling
[ ] Landmarks and page title identify the page
[ ] Native buttons, links, labels, lists, and tables are used
Keyboard and focus
[ ] Main task works with Tab, Shift+Tab, Enter, Space, Escape
[ ] Focus is always visible and never trapped by accident
[ ] Dialog focus enters, stays, and returns correctly
Content and forms
[ ] Controls have useful names; states update programmatically
[ ] Errors identify the field, problem, and repair
[ ] Color is not the only signal; contrast is measured
[ ] Images have contextual alt text; media has accurate captions
Reality check
[ ] 200% zoom and narrow viewport do not hide actions
[ ] Automated findings are reviewed, not treated as a score
[ ] A real screen reader completes the main and error paths
[ ] Known limitations have owners and release decisions
La mejor arquitectura de accesibilidad suele ser aburrida: controles nativos, orden de documento sensato, foco visible, etiquetas claras y mensajes de estado cortos. Construye esa base primero. Los widgets a medida y ARIA deberían ser la excepción que puedas explicar y probar, no el estilo por defecto del desarrollo frontend.