Le das a CSS un color de fondo y te devuelve blanco o negro, el que mejor se lea encima. Eso es contrast-color(), y desde abril de 2026 funciona en Chrome, Firefox y Safari.
Juega un minuto con la demo de arriba. La tarjeta grande no tiene color de texto definido en ningún sitio: arrastra el control de luminosidad, o pulsa Barrido, y mira cómo el texto salta de blanco a negro justo cuando el negro empieza a ganar. La tira de doce cuadros de abajo es la misma idea de un vistazo: misma luminosidad y mismo croma, doce tonos, y cada uno cambia en un punto distinto, porque un amarillo y un azul no tienen la misma luminancia con la "misma" luminosidad.
Qué es y por qué importa
Siempre que un color viene de los datos (el color de una etiqueta en un CMS, un tema que elige el usuario, una serie de un gráfico, el color de marca de un equipo) necesitas un color de texto que siga siendo legible encima. Hasta ahora eso era un helper en JavaScript, una función de Sass en tiempo de compilación o una tabla de parejas revisadas a mano. contrast-color() lleva esa decisión al navegador, en tiempo de valor calculado, así que también sigue a las custom properties, a los estados :hover y a los cambios de tema.
Hay un ejemplo real en este mismo repo. La etiqueta activa del explorador de notas de esta web usa bg-richi-orange text-white, es decir, blanco sobre #FF5200. Con la fórmula de WCAG 2.1 esa pareja da 3,25:1; el negro sobre el mismo naranja da 6,46:1. contrast-color(#FF5200) devuelve negro. La demo arranca justo en ese naranja para que lo veas.
Cómo funciona
La versión mínima son tres líneas, y es lo que usa la tarjeta de la demo:
.badge {
--bg: #FF5200;
background: var(--bg);
color: contrast-color(var(--bg));
}
La especificación CSS Color Level 5 es corta a propósito. La función recibe un <color> y se resuelve en white o black, el que dé el máximo contraste con ese color usado como fondo sólido. Si empatan, devuelve white. Y ya: ni ratio objetivo, ni lista de candidatos, ni colores propios. El registro de cambios de la especificación recoge que la función se simplificó por una resolución del grupo de trabajo, y una nota dice que se espera que versiones futuras den más control sobre el algoritmo y el color devuelto.
Dos detalles de la especificación que conviene saber:
- El algoritmo lo decide el navegador. La especificación dice que el algoritmo exacto de contraste lo define cada navegador, y les aconseja no usar sin más el ratio de contraste de WCAG 2.1 por sus problemas conocidos. Eso sí, exige que el resultado cumpla WCAG 2.1 AA para texto grande (3:1).
- Decide, no garantiza. Elige la mejor de dos opciones. No puede arreglar un fondo malo.
Por curiosidad comprobé si los motores actuales siguen WCAG de todas formas. En Chromium 153 y en WebKitGTK 2.52 comparé contrast-color() con la regla de WCAG 2.1 de "quedarse con el ratio más alto" en 2.000 colores sRGB aleatorios, y en los dos coincidieron siempre. Es una prueba, no una promesa: la especificación permite que cambie.
La trampa de los tonos medios
Esta es la pega de la que avisa MDN: en los tonos medios, ni el blanco ni el negro quedan bien. El ejemplo de la propia MDN es #2277d3, un azul real que recibe texto negro. Con la fórmula de WCAG eso es 4,65:1 para el negro y 4,52:1 para el blanco: técnicamente los dos pasan AA para texto normal, y los dos se leen incómodos en letra pequeña.
Además hay un suelo fijo. Haciendo las cuentas con la fórmula de luminancia de WCAG, lo mejor entre blanco y negro nunca baja de √21 ≈ 4,58:1. Así que contrast-color() nunca suspende AA del todo, pero en los tonos medios se queda rozando la línea. La demo enseña los dos ratios en vivo y te dice si estás por encima de 7:1 (AAA) o justo en AA. El arreglo sigue siendo de diseño: aclarar u oscurecer el fondo.
Soporte, plan B y un detalle que muerde
Soporte según los datos de compatibilidad de MDN: Chrome 147, Firefox 146 y Safari 26. web-features lo marca como Baseline (recién disponible) desde el 10 de abril de 2026. Probé la demo en Chromium 153 y en WebKitGTK 2.52; Firefox no lo pude ejecutar en mi entorno.
Para navegadores antiguos, el truco clásico de "declarar antes un valor de reserva" tiene trampa cuando usas una custom property. Esto no hace lo que esperas:
.badge {
color: black;
color: contrast-color(var(--bg)); /* se acepta al parsear por llevar var() */
}
Como el valor contiene var(), el navegador acepta la segunda declaración al parsear sin comprobar la función. Si luego no sabe resolver contrast-color(), la propiedad pasa a ser inválida en tiempo de valor calculado y cae al color heredado, no a tu black. Lo probé en Chromium con un nombre de función inventado y pasa exactamente eso. Usa @supports:
.badge { color: black; }
@supports (color: contrast-color(red)) {
.badge { color: contrast-color(var(--bg)); }
}
Y si necesitas la misma decisión en JavaScript (la demo lo hace como plan B cuando CSS.supports('color', 'contrast-color(red)') da false), la versión con WCAG 2.1 es corta:
const lin = (v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
function luminance(hex) {
const [r, g, b] = [1, 3, 5].map((i) => lin(parseInt(hex.slice(i, i + 2), 16) / 255));
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function contrastRatio(a, b) {
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
return (hi + 0.05) / (lo + 0.05);
}
// La misma regla que contrast-color(): gana el de más contraste, el empate es para el blanco
function pickTextColor(bg) {
return contrastRatio(bg, '#000000') > contrastRatio(bg, '#ffffff') ? 'black' : 'white';
}
pickTextColor('#FF5200'); // "black"
Qué vigilar
- El siguiente nivel de la función. La especificación ya dice que se espera más control sobre el algoritmo y los colores devueltos. Si necesitas colores de marca en lugar de blanco y negro puros, es ahí donde aparecería.
- El propio algoritmo. Como lo define cada navegador y la especificación desaconseja WCAG 2.1 a secas, un navegador podría pasarse a un modelo perceptual. Los fondos de tono medio son donde se notaría.
- Tus auditorías. Los validadores automáticos calculan ratios de WCAG 2.1. Si algún día un navegador elige distinto que WCAG en un tono medio, el validador y la pantalla podrían no coincidir.