Accesibilidad
Accesibilidad
22 reglasContraste mínimo WCAG AA
Texto normal: ratio 4.5:1 contra su fondo. Texto grande (>=24px o >=18.66px bold): ratio 3:1. Elementos no-texto (bordes de input, iconos funcionales): ratio 3:1 contra fondo adyacente. Validar con DevTools o axe-core, no a ojo.
WCAG 1.4.3 · 1.4.11Focus visible en todos los interactivos
Cada botón, link, input, select y elemento custom con tabindex necesita un estado :focus-visible distinguible. Ring de 2px en color de acento con outline-offset: 2px. Nunca outline: none sin reemplazo.
outline: 2px solid accent;
outline-offset: 2px;
}
Alt en imagenes, aria-label en icon-only buttons
Toda <img> lleva alt: descriptivo si es informativa, vacio (alt="") si es decorativa. Botones con solo icono necesitan aria-label que describa su acción. SVGs decorativos llevan aria-hidden="true". Sin estas etiquetas, los lectores de pantalla anuncian "botón" o "imagen" sin contexto, y el usuario no sabe que hace el control ni que muestra la foto.
alt: Equipo celebrando en la cancha
sin alt
Orden de tab lógico
El orden de tabulación debe seguir la lectura visual: izquierda a derecha, arriba a abajo. No usar tabindex positivo (1, 2, 3) para forzar orden, sino reestructurar el DOM para que el flujo natural coincida con el layout. Usar tabindex="0" para agregar un elemento al flujo de tab y tabindex="-1" para elementos que solo deben ser focusables vía JavaScript.
WCAG 2.4.3Touch targets mínimo 44x44px
Botones, links y controles interactivos necesitan un área de tap mínima de 44x44 CSS pixels en mobile (WCAG 2.5.8 Target Size Minimum). Cuando el elemento visual es más chico (icono de 24px), se expande el hit área con padding, min-width/min-height o un pseudo-elemento ::after con posición absoluta e inset negativo. Targets pequeños causan hasta un 20% de taps fallidos en estudios de usabilidad, especialmente en usuarios con temblor o motricidad reducida.
Nunca transmitir significado solo con color
Cualquier información transmitida por color debe tener un segundo canal: texto, icono, patrón o posición. Aplica a estados, gráficas, tags, badges y links. El daltonismo afecta al 8% de hombres, lo que vuelve indistinguibles combinaciones como rojo-verde o ambar-verde. Tres canales simultaneos (color + icono + etiqueta) garantizan que ningun usuario quede excluido.
WCAG 1.4.1Semántica HTML correcta
Los elementos semánticos de HTML (<nav>, <main>, <header>, <button>, <h1>-<h6>) no son decorativos: cada uno transmite un rol y una estructura que los screen readers anuncian automáticamente como landmarks, headings o controles interactivos. Cuando todo el markup es <div>, el arbol de accesibilidad queda plano y el usuario de tecnologia asistiva pierde la capacidad de saltar entre secciones, identificar la jerarquía del contenido y operar controles con teclado. Usar el elemento correcto es más simple que parchearlo con ARIA y garantiza compatibilidad nativa con todos los agentes de usuario.
Labels en todos los inputs
Cada <input>, <select> y <textarea> necesita un <label for="id"> asociado programaticamente. El placeholder desaparece en cuanto el usuario escribe, así que no sustituye al label: el usuario pierde contexto del campo y el screen reader no puede anunciar su nombre. Si el diseño no contempla label visible, usar aria-label o <label class="sr-only"> para mantener la accesibilidad sin alterar el layout.
Lighthouse Accessibility >= 90
Correr Lighthouse en cada página antes de entregar. Apuntar a score 90+. Lighthouse no cubre todo (no detecta orden de tab lógico ni calidad de alt text), pero atrapa lo básico: contraste, labels, aria, landmarks, heading order. Integrar la auditoria en CI para bloquear merges que bajen del umbral.
checklist pre-entregaprefers-reduced-motion y prefers-color-scheme
Respetar ambas preferencias del sistema. Reduced-motion: contenido siempre visible, transiciones gateadas por @media (prefers-reduced-motion: no-preference). Color-scheme: dark mode real, no inversión. Animaciones ambient pueden usar .motion-safe para bypass selectivo. Ignorar estas preferencias causa nausea en usuarios con trastornos vestibulares y deja interfaces ilegibles en dark mode forzado.
.card { opacity: 1; transform: none; }
@media (prefers-reduced-motion: no-preference) {
.card { transition: transform .3s; }
.loader { animation: spin 1s infinite; }
}
Keyboard navigable al 100%
Toda funcionalidad accesible por mouse debe ser accesible por teclado. Dropdowns navegan con flechas. Custom selects soportan Home/End/Type-ahead. Modales atrapan el foco (focus-trap). Escape cierra. Ningun dead-end de teclado: si puedes hacer click, puedes hacer Tab + Enter.
WCAG 2.1.1 · 2.1.2Mensajes de error descriptivos e inline
Los errores de formulario deben aparecer como texto descriptivo debajo del campo invalido, no como un banner genérico en la parte superior. Cada mensaje debe estar asociado al campo vía aria-describedby y el campo marcado con aria-invalid="true". El mensaje debe ser específico y accionable: indicar que esta mal y como corregirlo. Al enviar un formulario con errores, el foco debe moverse al primer campo invalido con scroll-into-view. Un toast genérico "Hubo un error" obliga al usuario a adivinar que campo fallo y es inaccesible para lectores de pantalla.
Toggle buttons: aria-pressed vs role switch
Dos primitivos distintos para comunicar estado binario. aria-pressed="true/false" sirve para toggle genérico (bold, mute). role="switch" + aria-checked es para on/off explicito (dark mode, notificaciones). Un toggle visual sin ninguno de estos atributos deja al screen reader sin información de estado: el usuario no sabe si esta encendido o apagado. Nunca cambiar label Y estado simultaneamente porque el screen reader anuncia ambos cambios y confunde.
Disclosure: aria-expanded en el botón
El atributo aria-expanded pertenece al elemento <button> que actua como trigger del disclosure, nunca al panel de contenido. La tecnologia asistiva lee el estado del botón (expandido o colapsado) en cada interacción, permitiendo al usuario saber si el contenido asociado esta visible. Usar un <div> clickeable en lugar de un <button> real impide que el lector de pantalla anuncie el rol y el estado del control.
Tab panels: roving tabindex + flechas
Tab activa = tabindex="0", inactivas = tabindex="-1". Flechas izq/der navegan entre tabs sin mover el foco fuera del grupo. La tecla Tab salta directo al panel activo, no atraviesa cada pestana. El panel necesita aria-labelledby apuntando al tab correspondiente. El wrapper li lleva role="presentation" para neutralizar semántica de lista.
Notificaciones: live regions
Usar role="status" con aria-live="polite" para notificaciones informativas (confirmaciones, guardado exitoso). Reservar role="alert" con aria-live="assertive" exclusivamente para errores criticos que requieren acción inmediata. Sin live regions, los toasts son completamente invisibles para screen readers: el usuario nunca se entera de que algo paso. Nunca mover el foco al toast; prefijar con tipo visible ("Exito:", "Error:").
Tablas ordenables: aria-sort
Las columnas ordenables de una tabla deben declarar role="columnheader" y aria-sort con valor ascending, descending o none. Sin estos atributos, la tecnologia asistiva anuncia los encabezados como texto plano y el usuario no sabe cual columna esta ordenada ni en que dirección. La columna activa actualiza aria-sort al valor correspondiente; las demas mantienen none.
| Nombre aria-sort=none | Fecha aria-sort=none | Monto aria-sort=descending |
|---|---|---|
| Celeste | 03 jun | $2,450 |
| Aurora Pro | 12 jun | $1,200 |
| Borealis | 08 jun | $890 |
| Nombre | Fecha | Monto |
|---|---|---|
| Aurora Pro | 12 jun | $1,200 |
| Borealis | 08 jun | $890 |
| Celeste | 03 jun | $2,450 |
(texto plano, no encabezados ordenables)
Menú buttons: menú vs nav dropdown
role="menú" + menuitem es para ACCIONES (ejecutar comandos), no para navegación. Los nav dropdowns usan ul + a sin role="menú". Un menú real requiere: aria-haspopup="true", navegación con flechas, y Escape que cierra y devuelve el foco al trigger. Mezclar links y acciones en un mismo menú confunde al usuario de AT, que no sabe si activar un item navegara o ejecutara un comando.
Tooltips vs toggletips
Un tooltip muestra texto descriptivo al hacer hover o focus, usa role="tooltip" con aria-describedby y no contiene elementos interactivos. Un toggletip se activa con click, persiste hasta que se cierra, usa aria-live="polite" y puede contener enlaces u otros elementos interactivos. Si necesitas poner un enlace dentro de un popup, es un toggletip, no un tooltip. Mezclarlos rompe la accesibilidad de teclado y tactil.
inclusive-components.designCards: link solo en el heading
No envolver toda la card en <a>: el screen reader lee todo el contenido como label del enlace, generando un bloque enorme e innavegable. Colocar el link solo en el heading (h3) produce un anuncio conciso y permite al usuario de AT saltar entre cards con Tab. Aplicar :focus-within en la card para feedback visual cuando el link interno recibe foco. Acciones secundarias como "Ver detalles" van como links independientes fuera del heading.
Autocomplete/combobox ARIA
Un combobox accesible combina un input con role="combobox", aria-expanded y aria-activedescendant para que el foco DOM permanezca siempre en el input mientras AT anuncia la opción resaltada. El listbox desplegable usa role="listbox" con cada item como role="option" y aria-selected. Arrow down abre la lista, Enter selecciona, Escape cierra. Al filtrar, una live region anuncia cuantos resultados quedan. Sin estos roles, las sugerencias son invisibles para tecnologias de asistencia.
Skeleton screens: aria-busy
Los elementos "hueso" decorativos llevan aria-hidden="true". El contenedor padre recibe aria-busy="true" mientras carga para que el screen reader no lea contenido parcial o viejo debajo del skeleton. Una live region con aria-live="polite" anuncia "Cargando..." al inicio y "Contenido actualizado" al completar. Cuando el contenido esta listo, aria-busy pasa a false y la live region notifica el cambio sin interrumpir la lectura activa del usuario.
- R-162 Contraste mínimo WCAG AA
- R-163 Focus visible en todos los interactivos
- R-164 Alt en imagenes, aria-label en icon-only buttons
- R-165 Orden de tab lógico
- R-166 Touch targets mínimo 44x44px
- R-167 Nunca transmitir significado solo con color
- R-168 Semántica HTML correcta
- R-169 Labels en todos los inputs
- R-170 Lighthouse Accessibility >= 90
- R-171 prefers-reduced-motion y prefers-color-scheme
- R-172 Keyboard navigable al 100%
- R-173 Mensajes de error descriptivos e inline
- R-174 Toggle buttons: aria-pressed vs role switch
- R-175 Disclosure: aria-expanded en el botón
- R-176 Tab panels: roving tabindex + flechas
- R-177 Notificaciones: live regions
- R-178 Tablas ordenables: aria-sort
- R-179 Menú buttons: menú vs nav dropdown
- R-180 Tooltips vs toggletips
- R-181 Cards: link solo en el heading
- R-182 Autocomplete/combobox ARIA
- R-183 Skeleton screens: aria-busy