wandres.dev
COLOR III · color-mix, colores relativos, gamut

color-mix() y el espacio de interpolación explícito

La gramática de la función, las reglas exactas de normalización de porcentajes, cómo elegir el espacio en el que se mezcla, y los patrones que resuelve sin JavaScript.

⏱ 18 min

color-mix() parece la función de mezclar dos colores y en realidad es otra cosa: es la primera vez que CSS te deja elegir en qué espacio ocurre una operación de color. Ese parámetro obligatorio, que muchos copian sin mirar, es el que decide si tu mezcla pasa por un gris turbio o mantiene el color. Y las reglas de normalización de porcentajes esconden un mecanismo para producir transparencia que no está documentado en ningún tutorial.

🎯 Al terminar esta lección sabrás
  • Escribir color-mix() con su método de interpolación y sus dos colores.
  • Predecir el resultado cuando los porcentajes no suman 100.
  • Elegir entre espacio cartesiano y polar según el caso.
  • Resolver tintes, sombras y superficies sin declarar colores nuevos.

La sintaxis y por qué el espacio es obligatorio

.a { background: color-mix(in oklch, #3364db, white); }
.b { background: color-mix(in oklch, #3364db 30%, white); }
.c { background: color-mix(in oklab, #3364db 30%, white 70%); }
.d { background: color-mix(in oklch longer hue, red, blue); }

Primero el método de interpolación, después los dos colores, cada uno con un porcentaje opcional. El método no tiene valor por defecto: color-mix(red, blue) no es una declaración válida y el navegador la descarta entera.

Esa obligatoriedad es una decisión de diseño deliberada y vale la pena entenderla. Mezclar dos colores es promediar sus coordenadas, y el resultado del promedio depende por completo del sistema de coordenadas. El punto medio entre azul y amarillo es un gris en sRGB, un verde apagado en Oklab y un verde vivo en OKLCH girando por el lado corto. Los tres son “la mezcla al 50%” y son colores distintos. Como no existe una respuesta correcta universal, la especificación se negó a elegir por ti y te obliga a decir cuál quieres. Es el mismo motivo por el que la función no se llama mix: no mezcla pintura, interpola coordenadas.

Los espacios aceptados son los rectangulares —srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020, lab, oklab, xyz, xyz-d50, xyz-d65— y los polares —hsl, hwb, lch, oklch—. Solo los polares aceptan un método de tono detrás.

Cómo se normalizan los porcentajes

Aquí está la parte que sorprende. Las reglas son tres y funcionan en este orden:

Si solo das un porcentaje, el otro es el complemento. color-mix(in oklch, red 30%, blue) es 30% de rojo y 70% de azul.

Si los dos suman más de 100, se escalan proporcionalmente. color-mix(in oklch, red 60%, blue 60%) se convierte en 50% y 50%. La proporción se conserva: red 75%, blue 25% sigue siendo tres partes de rojo por una de azul.

Si los dos suman menos de 100, se escalan proporcionalmente y el resultado recibe un alfa multiplicado por esa suma. Esta es la regla que casi nadie conoce y la que abre una puerta:

/* Mezcla 50/50 de rojo y azul, con alfa 0.8. */
.a { background: color-mix(in oklch, red 40%, blue 40%); }

/* El mismo color, escrito con la sintaxis relativa. */
.b { background: oklch(from color-mix(in oklch, red, blue) l c h / 0.8); }

La segunda forma usa la sintaxis de color relativo, que es el tema de la lección siguiente.

Si los dos porcentajes son cero, la declaración es inválida.

De la tercera regla sale el atajo para producir una versión traslúcida de cualquier color sin conocer su formato:

.velo { background: color-mix(in srgb, currentColor 12%, transparent); }

transparent es rgb(0 0 0 / 0), así que la mezcla se hace en espacio premultiplicado y el resultado es el color de partida con alfa 0.12. Es el equivalente moderno de la costumbre de escribir rgba(0, 0, 0, 0.12), con la diferencia de que aquí no necesitas saber los canales del color original.

⚠️
Para mezclar con transparent, usa un espacio rectangular

La mezcla con transparent en un espacio polar puede arrastrar el tono negro de transparent, que es tono cero, y girar la mezcla hacia el rojo. Con in srgb o in oklab el problema no existe. Si te empeñas en oklch, escribe el segundo color como oklch(from currentColor l c h / 0) en vez de transparent: ahí los tres canales coinciden y solo se mueve el alfa.

Elegir el espacio

La decisión se reduce a dos preguntas.

Primera: ¿alguno de los dos colores está cerca del gris? Si la respuesta es sí, elige un espacio rectangular. En un espacio polar el ángulo de tono de un color casi acromático es ruido numérico, y arrastrarlo por toda la mezcla produce tintes que no habías pedido. oklab es la respuesta por defecto para mezclar cualquier cosa con blanco, negro o gris.

Segunda: ¿quieres que la mezcla mantenga la intensidad del color? En oklch el croma se interpola directamente, así que el punto medio entre dos colores saturados sigue siendo saturado. En oklab el punto medio es la media de las coordenadas cartesianas, que está más cerca del eje gris. Para un degradado entre dos colores de marca, oklch mantiene el brillo; para atenuar un color, oklab es más predecible.

Y una tercera consideración que rara vez se menciona: in srgb sigue teniendo su sitio. Cuando necesitas reproducir exactamente el resultado de una mezcla hecha por otra herramienta —un diseño, una librería antigua, un valor calculado en el servidor— el espacio que esa herramienta usó era casi con seguridad sRGB. Reproducir un valor conocido es una razón legítima para usar un espacio malo.

Para los espacios polares hay además cuatro métodos de tono: shorter hue por defecto, longer hue para dar la vuelta larga, e increasing hue y decreasing hue para forzar el sentido del giro. Entre 20 y 340 grados, shorter recorre 40 grados pasando por el rojo y longer recorre 320 pasando por todo lo demás.

Los patrones que resuelve

Superficies derivadas de una sola base. El fondo de una tarjeta, el de su cabecera y el de su pie no son tres colores: son tres puntos de la misma escala.

.tarjeta {
  --superficie: oklch(97% 0.005 264);

  background: var(--superficie);
  border: 1px solid color-mix(in oklab, var(--superficie), currentColor 12%);
}

.tarjeta > header {
  background: color-mix(in oklab, var(--superficie), currentColor 4%);
}

Estados sin declarar colores. Un botón cuyo :hover es el mismo color un poco más oscuro no necesita un token nuevo:

.boton {
  background: var(--marca);
  &:hover  { background: color-mix(in oklab, var(--marca), black 8%); }
  &:active { background: color-mix(in oklab, var(--marca), black 16%); }
}

Adaptación al tema sin duplicar reglas. Si mezclas contra Canvas y CanvasText, que son colores del sistema que cambian con el esquema de color, la misma declaración funciona en claro y en oscuro:

.separador {
  border-block-start: 1px solid color-mix(in oklab, Canvas 88%, CanvasText);
}

En tema claro Canvas es blanco y la mezcla da un gris claro; en oscuro Canvas es casi negro y la misma mezcla da un gris oscuro. Una declaración, dos temas.

Con currentColor dentro, color-mix no se resuelve hasta el final

El detalle que convierte esta función en algo más que azúcar sintáctico está en cómo se calcula. Cuando color-mix() contiene currentColor, el valor calculado de la propiedad no es un color: es la función color-mix() con currentColor todavía sin resolver. La mezcla se completa al determinar el valor usado, cuando ya se sabe qué color tiene ese elemento. La consecuencia es que una sola declaración de border-color escrita en el componente base sigue el color del texto por todo el árbol, en cualquier variante, en cualquier tema, en :hover y dentro de un contenedor que redefina color, sin que exista ninguna regla que las relacione. Escribes border: 1px solid color-mix(in oklab, currentColor 20%, transparent) una vez y ese borde es siempre una quinta parte del color del texto, sea cual sea. Compáralo con la alternativa: declarar el color del borde en cada variante, lo cual significa que añadir una variante nueva obliga a acordarse de añadir también su borde. Aquí no hay nada que recordar porque no hay nada que declarar. El mismo mecanismo funciona con los colores de sistema, que también se resuelven tarde, y por eso color-mix(in oklab, Canvas 88%, CanvasText) responde al esquema de color sin que ninguna consulta de medio intervenga. La regla general que se deduce: cuando puedas expresar un color como una función de otro que ya está en la cascada, hazlo, porque entonces el color deja de ser un valor que hay que mantener y pasa a ser una relación que se mantiene sola.

⚔️ Sustituye tokens por relaciones
  1. Coge un componente con cinco tokens de color y reduce a dos declarando los otros tres como mezclas.
  2. Comprueba en el inspector qué alfa produce color-mix(in srgb, red 25%, blue 25%).
  3. Escribe la misma mezcla en in oklab y en in oklch con dos colores saturados y compara los puntos medios.
  4. Haz un borde con currentColor mezclado y cambia el color del ancestro. Verifica que el borde sigue.