Cómo calcular el contraste de color según WCAG en JavaScript

La accesibilidad web exige que el texto sea legible sobre su fondo. WCAG define umbrales de contraste calculados con una fórmula que tiene en cuenta cómo el ojo humano percibe la luz.

Comprobador de contraste de color WCAG: dos muestras de color con la relación de contraste calculada

Si alguna vez has intentado leer texto gris claro sobre fondo blanco y has tenido que entrecerrar los ojos, ya sabes de qué va este artículo. La legibilidad de un texto depende del contraste entre su color y el del fondo, y las Web Content Accessibility Guidelines (WCAG) han formalizado cuánto contraste es suficiente —y cuánto es excelente.

Los dos niveles: AA y AAA

WCAG 2.1 define dos umbrales para el contraste de color:

  • Nivel AA (mínimo exigible): relación de contraste de 4,5:1 para texto normal y de 3:1 para texto grande (18 pt o 14 pt en negrita).
  • Nivel AAA (óptimo): relación de 7:1 para texto normal y de 4,5:1 para texto grande.

El negro puro sobre blanco puro tiene una relación de 21:1 —el máximo posible. Un gris medio como #767676 sobre blanco da aproximadamente 4,48:1, que falla AA por los pelos.

La relación de contraste

La fórmula es sencilla. Dados dos colores, calculas la luminancia relativa de cada uno y aplicas esto:

ratio = (L_clara + 0,05) / (L_oscura + 0,05)

Donde L_clara es la luminancia del color más claro y L_oscura la del más oscuro. El 0,05 que se suma a ambos existe para que el negro puro no haga explotar el denominador a cero.

La luminancia relativa

Aquí está la parte no obvia. La luminancia relativa no es simplemente el promedio de los canales RGB: el ojo humano es mucho más sensible al verde que al azul, y la pantalla tampoco emite luz de forma lineal.

La fórmula de WCAG pondera los canales así:

L = 0,2126 × R_lin + 0,7152 × G_lin + 0,0722 × B_lin

Los coeficientes reflejan la sensibilidad del ojo: el verde domina, el rojo contribuye algo, el azul apenas pesa.

La linealización gamma

El punto más delicado: los valores RGB que usamos en CSS (#ff0000, rgb(255, 0, 0)) están codificados en el espacio de color sRGB, que aplica una corrección gamma para adaptarse a cómo funcionaban los monitores CRT. Antes de calcular la luminancia hay que invertir esa corrección y obtener valores lineales.

La fórmula tiene dos tramos porque la curva gamma tiene una parte lineal al inicio para evitar ruido en las sombras:

function linearizar(canal) {
  var v = canal / 255;
  return v <= 0.04045
    ? v / 12.92
    : Math.pow((v + 0.055) / 1.055, 2.4);
}

Si te saltas este paso y usas los valores sRGB directamente, los contrastes calculados serán incorrectos —sobre todo en colores oscuros.

La luminancia en JavaScript

Con la linealización lista, calcular la luminancia es directo:

function luminancia(hex) {
  var r = parseInt(hex.slice(1, 3), 16);
  var g = parseInt(hex.slice(3, 5), 16);
  var b = parseInt(hex.slice(5, 7), 16);
  return 0.2126 * linearizar(r)
       + 0.7152 * linearizar(g)
       + 0.0722 * linearizar(b);
}

El ratio final

function ratioContraste(hex1, hex2) {
  var l1      = luminancia(hex1);
  var l2      = luminancia(hex2);
  var lighter = Math.max(l1, l2);
  var darker  = Math.min(l1, l2);
  return (lighter + 0.05) / (darker + 0.05);
}

No importa el orden en que pases los colores: la función siempre identifica cuál es el más claro.

Comprobación de los umbrales

Con el ratio calculado, evaluar los criterios WCAG es trivial:

var ratio = ratioContraste('#595959', '#ffffff'); // → ~7.0

var aaTextoNormal  = ratio >= 4.5;  // true
var aaTextoGrande  = ratio >= 3;    // true
var aaaTextoNormal = ratio >= 7;    // true (justo en el límite)
var aaaTextoGrande = ratio >= 4.5;  // true

Conectar con un formulario HTML

La parte visual es conectar los <input type="color"> y los campos de texto hexadecimal con la función de render. El truco habitual es mantener sincronizados ambos: cuando el usuario mueve el selector de color nativo se actualiza el hex; cuando escribe un hex válido se actualiza el selector.

nativeInput.addEventListener('input', function () {
  hexInput.value = nativeInput.value.toUpperCase();
  render();
});

hexInput.addEventListener('input', function () {
  if (/^#[0-9a-fA-F]{6}$/.test(hexInput.value)) {
    nativeInput.value = hexInput.value;
    render();
  }
});

Y la función render actualiza la vista previa con element.style.color y element.style.background, y marca cada criterio como superado o fallido.

A continuación puedes probar la herramienta completa.

Abrir en página propia

Herramienta · paigar.eu

Contraste de color WCAG

Elige el color del texto y el del fondo para comprobar si superan los umbrales de accesibilidad WCAG 2.1.

Aa — Texto de muestra

Texto a tamaño normal · Comprobador de contraste WCAG · paigar.eu

Relación de contraste
Nivel AA
Texto normal ≥ 4,5:1
Texto grande ≥ 3:1
Nivel AAA
Texto normal ≥ 7:1
Texto grande ≥ 4,5:1
Prueba con estos ejemplos
21:1 · AAANegro / blanco
~15:1 · AAATinta / papel
~7:1 · AAGris oscuro
~5,3:1 · AABlanco / rojo
~2,9:1 · fallaNaranja / blanco
···
Otras entradas