Saltar a contenido

WA4.1 Animaciones y accesibilidad

Cómo se han comprobado los ejemplos

Los ejemplos con editor en vivo se ejecutan en tu navegador y se han comprobado en un navegador real. El JavaScript sin DOM (cálculo de contraste, debounce, throttle) se ha ejecutado con Node al construir la web y su salida es la real. Las cifras de rendimiento (por ejemplo, los umbrales de Core Web Vitals) las fija Google y pueden revisarse: consulta web.dev para los valores actuales.

Transiciones: cambiar suavemente entre dos estados

Una transición hace que, cuando una propiedad cambia de valor (por ejemplo al pasar el ratón), el cambio no sea instantáneo sino gradual.

La propiedad transition lleva: qué propiedades animar, cuánto dura y la curva (ease, linear, ease-in-out, cubic-bezier(...)). Se escribe en el estado de partida (el normal), de modo que anima tanto «hacia» el estado :hover como «de vuelta».

Anima transform y opacity, y casi nada más

El navegador puede animar transform (mover, escalar, girar) y opacity directamente en la tarjeta gráfica, sin recalcular la página. Animar width, height, top, left o margin obliga a recalcular el diseño en cada fotograma y se ve a tirones, sobre todo en móviles. Para mover algo, transform: translateX(...) en lugar de left.

Animaciones con @keyframes

Para algo más que un cambio entre dos estados (repetirse, tener pasos intermedios, empezar solo), se definen fotogramas clave y se aplican con animation:

animation: latir 1s ease-in-out infinite significa: nombre, duración, curva y cuántas veces (infinite: sin parar). both hace que se respeten el primer y el último fotograma fuera de la animación (aquí: que no se vea el elemento antes de empezar).

Respetar a quien no quiere movimiento

Para algunas personas (con trastornos vestibulares, migrañas, epilepsia o simplemente cansancio), el movimiento en pantalla produce mareo o malestar. Los sistemas operativos tienen un ajuste «reducir movimiento», y CSS lo lee con prefers-reduced-motion:

Una buena costumbre es desactivar o simplificar las animaciones dentro de ese bloque (quitar las que se repiten sin parar, los desplazamientos grandes y los parpadeos). No hace falta eliminar todo: un cambio de color es inofensivo; lo que marea es el movimiento grande y repetido.

Nada que parpadee más de tres veces por segundo

Un contenido que parpadea con frecuencia puede provocar crisis epilépticas fotosensibles. Es una de las pocas normas de accesibilidad que no admite excepciones.

Teclado: ¿se puede usar sin ratón?

Muchas personas usan la web solo con el teclado (por una discapacidad motora, por lector de pantalla, o porque les resulta más rápido). Comprobar una página es fácil: deja el ratón a un lado y usa Tab, Shift+Tab, Intro, Espacio y Escape.

Qué comprobar Cómo se consigue
Todo lo interactivo recibe el foco Usar los elementos nativos: <button>, <a href>, <input>, <select>
El foco se ve Nunca outline: none a secas; usa :focus-visible para controlarlo
El orden del foco es lógico Escribir el HTML en el orden en que se lee; no depender del CSS para reordenar
Puedes saltar la cabecera Un enlace «Saltar al contenido» al principio de la página
Un diálogo atrapa el foco El elemento <dialog> lo hace solo (ver más abajo)

tabindex

Valor Efecto
tabindex="0" El elemento entra en el orden natural del teclado (para widgets propios)
tabindex="-1" No se alcanza con Tab, pero se puede enfocar por código (el.focus())
tabindex="1" o mayor Nunca lo uses: altera el orden y lo vuelve imposible de mantener

La regla de oro: un <div onclick> no es un botón. No recibe el foco, no responde a Intro ni Espacio, y un lector de pantalla no lo anuncia como botón. Usa <button>.

Un enlace para saltar la navegación

El enlace está fuera de la pantalla (no con display: none, que lo haría inalcanzable) y aparece al recibir el foco. Quien usa el teclado evita tener que pasar por todo el menú en cada página.

<dialog>: la ventana modal que ya sabe hacer todo

Hacer una ventana modal accesible a mano (atrapar el foco, cerrar con Escape, devolver el foco al cerrar, bloquear el fondo) es difícil. El elemento <dialog> lo resuelve cuando se abre con showModal():

Con showModal() el navegador, sin código tuyo: pone el diálogo encima de todo con un fondo oscurecido (::backdrop), mueve el foco dentro (al elemento con autofocus), impide interactuar con el resto de la página, lo cierra con Escape y devuelve el foco al botón que lo abrió. Un <form method="dialog"> cierra el diálogo al enviar y deja en returnValue el value del botón pulsado.

ARIA: describir lo que HTML no puede

ARIA son atributos (role, aria-*) que dan información extra a los lectores de pantalla. Tiene una regla de oro que lo resume todo:

La primera regla de ARIA: si puedes usar un elemento nativo, no uses ARIA

Un <button> ya tiene rol, foco y teclado. Un <div role="button"> solo cambia lo que se anuncia: tendrías que programar tú el foco, Intro, Espacio... Y ARIA mal puesto es peor que no ponerlo: promete algo que no ocurre.

Los usos más habituales, cuando sí hacen falta:

Atributo Para qué
aria-label="Cerrar" Un nombre accesible cuando el elemento no tiene texto visible (un botón con solo un icono)
aria-labelledby="id" Que el nombre sea el texto de otro elemento
aria-expanded="true/false" Un botón que despliega algo: ¿está desplegado?
aria-controls="id" Qué elemento controla el botón
aria-live="polite" Una zona cuyos cambios se anuncian solos (avisos de «guardado», errores)
aria-hidden="true" Oculta un elemento decorativo a los lectores de pantalla
aria-current="page" El enlace de la página en la que estás

Fíjate en que el estado visible y el estado ARIA deben cambiar siempre juntos. Y en que la zona aria-live debe existir ya (vacía) en la página: si se crea a la vez que su texto, algunos lectores no anuncian nada.

Contraste de color

Un texto gris claro sobre fondo blanco es ilegible para mucha gente (baja visión, pantallas con reflejos, personas mayores). Las pautas de accesibilidad WCAG fijan un mínimo de contraste entre el color del texto y el del fondo:

Nivel Texto normal Texto grande (≥ 24 px, o ≥ 18,7 px en negrita)
AA (el mínimo que se suele exigir) 4,5 : 1 3 : 1
AAA (mejorado) 7 : 1 4,5 : 1

El contraste se calcula: primero la luminancia relativa de cada color, y después la proporción entre ambas. Es una fórmula pura, así que se ejecuta y se comprueba:

// Luminancia relativa de un color #rrggbb (fórmula de WCAG)
function luminancia(hex) {
  const [r, g, b] = [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16) / 255)
    .map(c => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);   // quita la «curva» de la pantalla
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;                            // el ojo es más sensible al verde
}

function contraste(a, b) {
  const [claro, oscuro] = [luminancia(a), luminancia(b)].sort((x, y) => y - x);
  return (claro + 0.05) / (oscuro + 0.05);
}

const pares = [["#000000", "#ffffff"], ["#777777", "#ffffff"], ["#767676", "#ffffff"], ["#ffffff", "#2563eb"], ["#ffffff", "#60a5fa"]];
for (const [texto, fondo] of pares) {
  const c = contraste(texto, fondo);
  console.log(texto, "sobre", fondo, "→", c.toFixed(2) + " : 1", c >= 4.5 ? "AA ✔" : c >= 3 ? "solo texto grande" : "NO cumple");
}

Salida (ejecutado con Node):

#000000 sobre #ffffff → 21.00 : 1 AA ✔
#777777 sobre #ffffff → 4.48 : 1 solo texto grande
#767676 sobre #ffffff → 4.54 : 1 AA ✔
#ffffff sobre #2563eb → 5.17 : 1 AA ✔
#ffffff sobre #60a5fa → 2.54 : 1 NO cumple

Un detalle famoso: #777777 sobre blanco da 4,48 : 1 y NO cumple el mínimo para texto normal; basta oscurecerlo un poco (#767676) para llegar a 4,54. En las herramientas del navegador (inspector de color) y en extensiones como Lighthouse o WAVE se mide sin escribir nada.

El color no debe ser lo único que informa

Un campo con error solo en rojo no lo ve una persona con daltonismo. Añade también un icono, un texto o un subrayado («Error: el correo no es válido»).

Errores frecuentes

Error Cómo evitarlo
<div onclick> en lugar de un botón <button>: foco, Intro y Espacio vienen incluidos
outline: none sin sustituto :focus-visible con un estilo propio y visible
tabindex positivo Solo 0 o -1
Animaciones constantes sin alternativa @media (prefers-reduced-motion: reduce)
Animar width, top o margin transform y opacity
Una ventana modal hecha a mano <dialog> con showModal()
ARIA que contradice o repite lo nativo Primero HTML nativo; ARIA solo lo que falta
Texto gris claro «porque queda elegante» Calcular el contraste (4,5 : 1 como mínimo)
Información solo por color Añadir icono o texto

Para practicar

Los ejercicios WA4.1 y WA4.2 de WA4 · Ejercicios calculan el contraste; el WA4.5 construye un menú desplegable accesible.