Cumplir el contraste de color de las WCAG sin adivinar
Para el texto de cuerpo normal, el nivel AA de las WCAG exige una relación de contraste de al menos 4,5:1 frente a su fondo. El texto grande necesita 3:1. Los componentes de interfaz y los gráficos con significado necesitan 3:1. El nivel AAA sube el texto a 7:1 y el texto grande a 4,5:1. Las relaciones van de 1:1 (colores idénticos) a 21:1 (negro puro sobre blanco puro).
Esto no se puede juzgar a ojo. Dos pares que parecen igual de legibles en tu monitor pueden quedar a lados opuestos del umbral, así que la relación hay que calcularla.
Los umbrales
| Qué es | AA | AAA |
|---|---|---|
| Texto normal | 4,5:1 | 7:1 |
| Texto grande | 3:1 | 4,5:1 |
| Componentes de interfaz y objetos gráficos | 3:1 | sin listón más alto |
Salen de tres criterios de conformidad: 1.4.3 Contraste (mínimo) en AA, 1.4.6 Contraste (mejorado) en AAA y 1.4.11 Contraste no textual en AA, que cubre los bordes visuales de los controles y las partes de un gráfico que hacen falta para entenderlo.
Texto grande significa al menos 18 puntos, o 14 puntos si va en negrita, lo que viene a ser unos 24px en redonda o unos 18,5px en negrita. No significa «un título»: un título de 20px es texto normal a efectos de las reglas y necesita los 4,5:1 completos.
Unas pocas cosas quedan exentas: el texto puramente decorativo, el texto de un control inactivo, el texto que forma parte de una imagen con otro contenido visual significativo y los logotipos. El texto de marcador de posición y el de ayuda no están exentos.
Qué mide en realidad la relación
La fórmula compara la luminancia relativa de los dos colores:
ratio = (L_lighter + 0.05) / (L_darker + 0.05)
La luminancia relativa es un número de 0 (negro) a 1 (blanco) que representa cuánta luz emite un color. Se calcula deshaciendo la curva gamma de sRGB en cada canal y ponderándolos después:
L = 0.2126 R + 0.7152 G + 0.0722 B (on linearised channels)
El 0.05 que se suma a los dos lados modela un poco de luz ambiente reflejada en la pantalla, y es la razón por la que el máximo es 21 y no infinito.
De ahí se siguen dos cosas. La relación depende solo de la luminancia, no del tono, así que dos colores pueden tener tonos radicalmente distintos y aun así ser casi invisibles el uno sobre el otro. Y es simétrica, así que da igual a cuál de los dos llames primer plano.
La luminancia relativa no es el brillo
Fíjate en esos pesos. El verde aporta alrededor del 72% de la luminancia, el rojo un 21% y el azul apenas un 7%. Eso es un modelo de la visión humana, no de lo vivo que parece un color, y produce resultados que chirrían hasta que te acostumbras a ellos.
- El amarillo puro
#FFFF00sobre blanco da alrededor de 1,07:1, casi invisible, pese a que el amarillo se percibe como un color fuerte. - Ese mismo amarillo sobre negro da alrededor de 19,6:1, casi tanto como el blanco sobre negro.
- El azul puro
#0000FFsobre negro da alrededor de 2,4:1 y suspende en todos los niveles, aunque se vea rotundo y saturado.
Esta es también la razón por la que la luminosidad de HSL es un mal sustituto. Dos colores pueden estar los dos al 50% de luminosidad en HSL y tener luminancias muy distintas, porque HSL trata el tono y la luminosidad como independientes y la visión no. Convierte y luego mide, en vez de dar por hecho que un salto de luminosidad es un salto de contraste.
Los errores que se repiten
Texto de cuerpo gris sobre blanco. Este es el gordo. El gris más claro que pasa AA sobre blanco es más o menos #767676, que da 4,54:1. Un paso más claro, #777777, da 4,48:1 y suspende. Los grises «apagados» populares, como #999999, se quedan cerca de 2,9:1, menos de dos tercios de lo exigido. Si quieres AAA sobre blanco, necesitas aproximadamente #595959, que da exactamente 7:1.
Texto de marcador de posición y de ayuda. Se les ponen varios tonos más claros que al texto de cuerpo porque son secundarios. Siguen siendo texto y siguen necesitando 4,5:1.
Colores semitransparentes. Una relación tomada del color declarado no significa nada si el elemento está al 60% de opacidad. Calcula el color compuesto que aparece de verdad en pantalla y mide ese.
Texto sobre imágenes, degradados y vídeo. La relación cambia de un píxel a otro y el píxel que importa es el peor. La solución habitual es un velo detrás del texto en lugar de confiar en que la foto se mantenga oscura.
Botones con el color de marca. El texto blanco sobre un color de marca de tono medio se queda muy a menudo entre 3:1 y 4,5:1: aprueba como objeto gráfico y suspende como etiqueta. Oscurece el botón, o asume el suspenso a sabiendas.
Indicadores de foco y botones solo con icono. Los dos caen bajo la regla no textual de 3:1. Un anillo de foco en un tono ligeramente distinto del mismo color es un fallo habitual, igual que un icono gris que es el único contenido de un botón.
Modo oscuro por inversión. Dar la vuelta a una paleta clara no conserva las relaciones, porque la luminancia no es simétrica respecto al centro. El blanco puro sobre negro puro da el máximo de 21:1, pero a mucha gente le resulta incómoda la halación, así que un blanco roto sobre un gris muy oscuro, como #E6E6E6 sobre #121212 a unos 15:1, es un término medio habitual.
Medir desde una captura de pantalla. Un cuentagotas en el borde de un texto suavizado toma una mezcla de texto y fondo, no el color del texto. Toma la muestra en el centro de un trazo grueso o, mejor todavía, lee los valores de tu CSS.
Un flujo de trabajo que evita las adivinanzas
Fija primero el fondo, luego oscurece o aclara el primer plano hasta que supere el umbral con algo de margen, y anota ese par. Construye la paleta a partir de pares aprobados, no de colores aprobados: un color no es accesible por sí solo, solo en combinación. Comprueba también todos los estados, no solo el de reposo, y los dos temas si publicas un modo oscuro.
Una última nota. Se está desarrollando un modelo perceptual más reciente, APCA, para una futura versión de las WCAG, y se comporta de forma bastante distinta, sobre todo con texto claro sobre fondos oscuros. Conviene conocerlo, pero las cifras con las que hay que cumplir hoy son las relaciones de las WCAG 2 de arriba.