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

La función color() y los espacios anchos

Qué espacios acepta color(), cuánto más color da de verdad Display P3 y dónde, cómo detectar el gamut de la pantalla, y para qué sirven los espacios de luz lineal.

⏱ 18 min

Las pantallas dejaron de ser sRGB hace años. Cualquier portátil o móvil decente cubre Display P3, y sin embargo la inmensa mayoría del CSS del mundo sigue escrito dentro de un gamut definido en 1996 para tubos de rayos catódicos. color() es la puerta de salida, y lo interesante no es que exista sino cuánto margen te da de verdad, porque la respuesta cambia radicalmente según el tono.

🎯 Al terminar esta lección sabrás
  • Escribir color() con los espacios predefinidos que acepta la especificación.
  • Cuantificar cuánto croma añade Display P3 sobre sRGB en cada región.
  • Detectar el gamut de la pantalla con @media y el soporte con @supports.
  • Explicar para qué sirve srgb-linear y por qué no es sRGB.

La función y sus espacios

.a { background: color(display-p3 1 0.2 0.1); }
.b { background: color(srgb 1 0.2 0.1); }
.c { background: color(rec2020 0.9 0.2 0.1 / 0.5); }
.d { background: color(xyz-d65 0.4 0.2 0.05); }

El primer argumento es el nombre del espacio y los tres siguientes son sus coordenadas, siempre en el rango 0 a 1 —no 0 a 255— seguidas del alfa opcional tras una barra. Los espacios predefinidos son:

Espacio Qué es
srgb El de siempre, con su función de transferencia
srgb-linear Los mismos primarios, sin la codificación gamma
display-p3 Primarios de cine digital, transferencia de sRGB
a98-rgb Adobe RGB 1998, habitual en fotografía
prophoto-rgb Muy ancho, punto blanco D50
rec2020 El de la televisión de ultra alta definición
xyz, xyz-d65 CIE XYZ con punto blanco D65
xyz-d50 CIE XYZ con punto blanco D50

color(srgb 1 0 0) es exactamente #ff0000. La función no cambia nada por sí sola: lo que cambia es qué primarios significan esos números.

Un detalle que ahorra confusión: color() es una función de espacio con primarios, mientras que oklch() es una función de espacio perceptual sin gamut propio. oklch(70% 0.3 150) describe un color que existe independientemente de qué pantalla lo mire; color(display-p3 0 1 0.4) describe “lo que sale al encender así los tres primarios de P3”. Los dos son válidos y sirven para cosas distintas: el primero para describir un color, el segundo para exprimir un dispositivo.

Cuánto más da Display P3

Aquí es donde la intuición suele fallar. “P3 es un 50% más grande que sRGB” es una cifra de área en el diagrama de cromaticidad y no te dice nada sobre el color que ibas a usar. Medido como croma máximo disponible a claridad media, el reparto es este:

Tono Croma máximo en sRGB En Display P3 Ganancia
29 (rojo) 0.246 0.277 12%
110 (amarillo) 0.131 0.152 16%
142 (verde) 0.202 0.238 18%
195 (cian) 0.102 0.138 35%
264 (azul) 0.217 0.233 7%
328 (magenta) 0.276 0.302 9%

La ganancia real va del 8% al 34% según dónde mires. En los azules P3 apenas aporta, porque su primario azul es prácticamente el mismo que el de sRGB. En los cianes y verdes es donde está casi todo el margen, que es justamente la región donde sRGB era más pobre.

Eso tiene una consecuencia de diseño concreta: si tu marca es azul, P3 no te va a dar nada. Si es verde azulada, te da un tercio más de intensidad y se nota mucho. Antes de reescribir una paleta entera en P3, mira dónde caen tus tonos.

Detectar el gamut

Hay dos preguntas distintas y se responden con dos mecanismos distintos.

¿Entiende el navegador esta sintaxis? Con @supports:

@supports (color: color(display-p3 1 0 0)) {
  .marca { --base: color(display-p3 0 0.62 0.55); }
}

¿Puede la pantalla mostrarlo? Con @media:

@media (color-gamut: p3) {
  .marca { --base: oklch(65% 0.16 175); }
}
@media (color-gamut: rec2020) {
  .marca { --base: oklch(65% 0.20 175); }
}

color-gamut acepta srgb, p3 y rec2020, y es acumulativa: una pantalla que cumple rec2020 también cumple p3 y srgb. La consulta pregunta si el dispositivo aproxima ese gamut, no si lo cubre exactamente.

En la práctica hace falta menos de lo que parece. Si escribes el color en oklch() y resulta estar fuera de sRGB pero dentro de P3, el navegador lo pinta en P3 en una pantalla que pueda y lo mapea en una que no. No necesitas la consulta de medio para eso: la necesitas solo si quieres dar valores distintos a propósito, por ejemplo un color más apagado en sRGB que el mapeo automático no habría elegido.

ℹ️
Un color fuera de sRGB no rompe nada

Escribir oklch(70% 0.28 150) es perfectamente válido aunque esté fuera de sRGB. En una pantalla P3 se ve con toda su intensidad; en una sRGB el navegador lo lleva al gamut disponible. No hay declaración inválida, no hay fallback que escribir y no hay nada que detectar. Lo único que cambia según la pantalla es cuánto se aprecia.

Los espacios lineales

srgb-linear tiene los mismos primarios y el mismo punto blanco que srgb, y una única diferencia: no aplica la función de transferencia. Sus números son proporcionales a la cantidad de luz, no a la señal codificada.

Eso importa porque casi toda la matemática de composición está definida sobre luz lineal. Sumar dos fuentes de luz es sumar sus intensidades; promediar la luz de dos regiones al reducir una imagen es promediar intensidades. Si haces esas operaciones sobre valores con gamma aplicada, el resultado sale sistemáticamente oscuro, que es el origen del clásico “el degradado se ve sucio en la mitad” y del “la imagen escalada pierde luz”.

/* Mezcla energetica: como se sumarian dos luces. */
.suma { background: color-mix(in srgb-linear, #ff0000, #00ff00); }

/* Mezcla perceptual: como un ojo interpola dos tonos. */
.perc { background: color-mix(in oklab, #ff0000, #00ff00); }

Las dos son correctas para preguntas distintas. srgb-linear responde “qué color sale si mezclo estas dos luces”. oklab responde “qué color está a medio camino visualmente entre estos dos”. Para interfaces quieres casi siempre la segunda; para simular iluminación, transparencias físicas o composición de capas, la primera.

xyz es el otro espacio lineal disponible, y es el que quieres cuando necesitas un intermediario neutro entre dos espacios de primarios distintos sin pasar por ningún modelo perceptual.

El gamut ancho no arregla el problema, lo mueve

Hay una expectativa razonable y equivocada sobre P3: que al ampliar el gamut desaparecen los problemas de mapeo. Lo que ocurre es que se desplazan, y en un aspecto se agravan. Primero, el borde sigue existiendo, solo que ahora está en otro sitio, y un color diseñado contra el borde de P3 se mapea en una pantalla sRGB exactamente igual de mal que antes: si tu color de acento vive fuera de sRGB, la mitad de tus usuarios lo ve desaturado y tú no. Segundo, y esto es lo que se pasa por alto, el mapeo entre gamuts no es una función que preserve distancias. Dos colores separados y bien distinguibles en P3 pueden acabar mapeados a colores casi idénticos en sRGB, porque los dos estaban en la región que P3 tiene y sRGB no. Es decir: puedes diseñar en P3 una escala de cinco pasos perfectamente legible y que en una pantalla sRGB tenga tres pasos indistinguibles. Ningún test automático te lo va a decir, porque el CSS es válido, el contraste calculado sobre los valores declarados es correcto, y el navegador no reporta nada. La única forma de detectarlo es mirar el diseño en una pantalla sRGB de verdad, cosa que casi nadie hace porque los equipos de diseño trabajan en portátiles con P3. De ahí sale la regla operativa: usa el gamut ancho para el acento y para las superficies grandes, nunca para distinguir estados o niveles de una escala. Un botón puede permitirse ser más vivo donde la pantalla lo permita; una escala de severidad de cinco niveles no puede permitirse que dos niveles se fundan. Lo que codifica información se diseña dentro de sRGB, siempre, y el gamut ancho se reserva para lo que solo tiene que verse bien.

⚔️ Mide tu gamut
  1. Pinta dos cuadrados con color(srgb 0 1 0) y color(display-p3 0 1 0) y míralos en tu pantalla. Si son iguales, tu pantalla es sRGB.
  2. Coge el color de marca de un proyecto tuyo, conviértelo a OKLCH y comprueba si su croma cabe en sRGB.
  3. Escribe una escala de cinco pasos usando croma de P3 y compárala forzando sRGB en las herramientas del navegador.
  4. Mezcla rojo y verde en srgb-linear y en oklab y explica la diferencia de claridad del resultado.