Product Craft Bible
Accesibilidad
Inicio Accesibilidad Accesibilidad
Accesibilidad

Accesibilidad

22 reglas WCAG 1.4.3 · 1.4.11WCAG 2.4.7 · 2.4.13WCAG 1.1.1 · 4.1.2WCAG 2.4.3
16

Accesibilidad

22 reglas
162

Contraste 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.11
Preferir
Contraste WCAG AA verificado
Nombre completo
7.1:1
Escribe tu nombre
Correo electronico
7.1:1
tu@email.com
borde 4.8:1
Agregar campo 5.2:1
Normal: 4.5:1 · Grande: 3:1 · No-texto: 3:1
Evitar
Formulario con contraste insuficiente
Nombre completo
2.1:1
Escribe tu nombre
Correo electronico
2.1:1
tu@email.com
borde 1.3:1
Agregar campo 1.8:1
163

Focus 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.

WCAG 2.4.7 · 2.4.13
Preferir
Iniciar sesión
nombre@ejemplo.com
••••••••
Entrar
Olvide mi contrasena
:focus-visible {
  outline: 2px solid accent;
  outline-offset: 2px;
}
Ring visible en cada interactivo
Evitar
Iniciar sesión
nombre@ejemplo.com
••••••••
Entrar
Olvide mi contrasena
outline:none sin reemplazo, usuario de teclado perdido
164

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.

WCAG 1.1.1 · 4.1.2
Preferir
Buscar
Configuración
Notificaciones
Perfil
Equipo celebrando en la cancha alt: Equipo celebrando en la cancha
Cada icono nombrado, cada imagen descrita
Evitar
sin aria-label
sin aria-label
sin aria-label
sin aria-label
sin alt
Screen reader: "botón", "botón", "botón", "botón", "imagen"
165

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.3
Preferir
1
2
3
4
Name(1) → Email(2) → Password(3) → Submit(4): flujo lógico
Evitar
1
2
3
4
Name(1) → Submit(2) → Email(3) → Password(4): orden caotico
166

Touch 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.

WCAG 2.5.8 · Apple HIG · Material Design
Preferir
44x44
44x44
44x44
44x44
WCAG 2.5.8: mínimo 44x44 CSS pixels
Evitar
24x24
24x24
24x24
24x24
Dedos grandes fallan 20% de taps
167

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.1
Preferir
Estado de servicios
API Principal Activo
Base de datos Error
CDN Pendiente
Email Inactivo
Icono + texto: inequivoco sin importar como perciba el color el usuario
Tres canales simultaneos: color, icono y etiqueta, ningun usuario queda excluido
Evitar
Estado de servicios
API Principal
Base de datos
CDN
Email
Daltonismo afecta al 8% de hombres: estos 4 colores se ven iguales
Color como único canal: los 4 dots son indistinguibles para usuarios con daltonismo
168

Semá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.

WCAG 1.3.1 · 4.1.2
Preferir
Mi App
header
Título principal h1
Contenido del articulo con información relevante para el usuario.
Leer más
button
Screen readers anuncian landmarks y roles automáticamente
Elementos semánticos: el arbol de accesibilidad expone estructura, landmarks y controles
Evitar
Mi App
div
Título principal div
Contenido del articulo con información relevante para el usuario.
Leer más
div
Todo es div: el screen reader ve un bloque plano sin estructura ni controles
169

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.

WCAG 1.3.1 · 3.3.2
Preferir
Label visible + for/id asociado = accesible
Evitar
Placeholder no es label: desaparece al escribir
170

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-entrega
Preferir
Lighthouse
Accesibilidad
96
Contraste: 0 errores
Labels y landmarks correctos
1 minor issue (color contrast on footer link)
Todas las auditorias pasadas
CI bloquea merge si < 90
Evitar
Lighthouse
Accesibilidad
54
Images without alt: 12
Links without name: 5
Low contrast: 8
Score critico, 25 auditorias fallidas
171

prefers-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.

WCAG 2.3.3 · MDN prefers-reduced-motion
Preferir
/* Base: estático, visible */
.card { opacity: 1; transform: none; }

@media (prefers-reduced-motion: no-preference) {
  .card { transition: transform .3s; }
  .loader { animation: spin 1s infinite; }
}
motion: no-preference Transición sutil
motion: reduce Estático, sin mareo
Animación gateada por media query
Evitar
Cargando datos
Spinner y entrada animada se reproducen sin importar la preferencia del usuario.
prefers-reduced-motion: reduce
Vestibular disorders: animación causa mareo
172

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.2
Preferir
Categoría
Electronica
Ropa
Hogar
Tab
Enter
↑ ↓
Esc
Home
End
Cada interacción posible con mouse, posible con teclado
Evitar
Categoría
Electronica
Ropa
Hogar
Tab
salta el dropdown
div con onClick: invisible para teclado
173

Mensajes 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.

WCAG 3.3.1 · 3.3.3
Preferir
Formato invalido: falta @ y dominio
aria-describedby
Mínimo 8 caracteres, tienes 4
aria-describedby
Inline, específico, accionable
Evitar
Hay errores en el formulario
El usuario tiene que adivinar que campo fallo
174

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.

inclusive-components.design
Preferir
Activado
Notificaciones
role="switch" aria-checked="true"
Desactivado
Notificaciones
role="switch" aria-checked="false"
Screen reader output
"Notificaciones, interruptor, activado"
Estado comunicado en cada cambio
Evitar
Notificaciones
sin role sin aria-checked
Screen reader output
"clickable"
Screen reader no sabe si esta encendido o apagado
175

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.

inclusive-components.design
Preferir
Detalles de envio
button
Detalles de envio
button aria-expanded=true
Envio gratuito en pedidos mayores a $999. Entrega estimada: 3-5 días habiles.
Detalles de envio, botón, expandido
aria-expanded comunica estado en cada toggle
Evitar
Detalles de envio
sin aria-expanded div no es button
Detalles de envio, text
AT no sabe si esta abierto o cerrado
176

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.

inclusive-components.design
Preferir
Perfil tabindex=0
Seguridad tabindex=-1
Notificaciones tabindex=-1
Contenido del panel activo: configuración de perfil
entre tabs | Tab al panel
Flechas entre tabs, Tab al contenido del panel
Evitar
Perfil tabindex=0
Seguridad tabindex=0
Notificaciones tabindex=0
Contenido del panel activo: configuración de perfil
Tab Tab Tab
3 tabs = 3 tab stops: lento y no estandar
177

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:").

inclusive-components.design
Preferir
Archivo guardado exitosamente
role=status aria-live=polite
Error: Sesión expirada. Inicia de nuevo.
role=alert aria-live=assertive
"Archivo guardado exitosamente"
polite para info, assertive para errores criticos
Evitar
Archivo guardado exitosamente
sin aria-live
(sin salida)
Notificación invisible para screen readers
178

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.

WAI-ARIA Authoring Practices (APG)
Preferir
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
role=columnheader aria-sort en cada th
Screen reader: "Monto, encabezado de columna, ordenado descendente"
aria-sort comunica estado de ordenamiento
Evitar
Nombre Fecha Monto
Aurora Pro 12 jun $1,200
Borealis 08 jun $890
Celeste 03 jun $2,450
sin aria-sort sin role=columnheader
Screen reader: "Nombre. Fecha. Monto."
(texto plano, no encabezados ordenables)
AT no sabe que columna esta ordenada ni en que dirección
179

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.

inclusive-components.design
Preferir
Inicio a
Nosotros a
Blog a
<ul><li><a>
Editar menuitem
Duplicar menuitem
Eliminar menuitem
<ul role="menú">
Links navegan, menús ejecutan acciones. Patrones ARIA distintos.
Evitar
App
Inicio link
Nosotros link
Eliminar acción
Archivar acción
<ul role="menú"> <!-- incorrecto -->
role="menú" es para acciones, no para navegación. Mezclar links y acciones en un mismo componente confunde al AT.
180

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.design
Preferir
Tooltip: texto descriptivo
Ajustes avanzados
role="tooltip" aria-describedby
Toggletip: contenido interactivo
Ajustes avanzados. Ver documentación
aria-live="polite" link reachable
Tooltip para texto, toggletip para contenido interactivo
Evitar
Configuración
Ajustes avanzados. Ver documentación
hover only link inside unreachable
Tooltip con contenido interactivo: imposible para teclado y touch
181

Cards: 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.

inclusive-components.design
Preferir
Proyecto Aurora
Última actividad hace 2 días
12 miembros, 85% completado
Screen reader anuncia:
"Proyecto Aurora, enlace"
Link solo en heading: navegable y conciso para AT
Evitar
Proyecto Aurora
Última actividad hace 2 días
12 miembros, 85% completado
Screen reader anuncia:
"Proyecto Aurora, Última actividad hace 2 días, 12 miembros, 85% completado, Ver detalles, enlace"
Toda la card como link: screen reader lee un bloque enorme como texto del link
182

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.

WAI-ARIA Authoring Practices 1.2 - Combobox
Preferir
Buscar ciudad
Meri role=combobox
Merida, Yucatan role=option aria-selected
Meridian, Idaho
Meribel, Francia
aria-expanded=true aria-activedescendant role=listbox
"Buscar ciudad, combobox, expandido, Merida seleccionado, 3 de 5"
Combobox pattern completo: input + listbox + activedescendant
Evitar
Buscar ciudad
Meri sin role=combobox
Merida, Yucatan sin aria-activedescendant
Meridian, Idaho
Meribel, Francia sin aria-expanded
Screen reader: "editar texto"
Sugerencias invisibles para AT
183

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.

inclusive-components.design
Preferir
Cargando (aria-busy=true)
aria-busy="true"
Listo (aria-busy=false)
aria-live="polite"
Maria Garcia
Diseño de producto
Carlos Ruiz
Ingenieria frontend
Screen reader: "Contenido actualizado"
aria-busy aria-live="polite"
aria-busy previene lectura parcial, aria-live anuncia cuando esta listo
Evitar
sin aria-busy sin aria-live
AT lee contenido viejo o nada mientras carga