Color moderno
Lección 34 de 37 · 4 min
Con oklch, color-mix y variables ya podés armar una paleta. CSS ahora suma funciones que derivan el resto del tema a partir de un solo color, y eligen solas entre claro y oscuro.
Un valor para los dos temas
light-dark() recibe dos colores: devuelve el primero si el elemento usa el esquema claro y el segundo si usa el oscuro. Quién decide es la propiedad color-scheme. Con color-scheme: light dark en :root, el navegador sigue la preferencia del sistema, la misma que lee @media (prefers-color-scheme: dark). Con color-scheme: dark forzás el oscuro en ese elemento y sus hijos. Sin color-scheme, siempre devuelve el claro.
:root {color-scheme: light dark;}body {background: light-dark(oklch(0.97 0.02 250), oklch(0.25 0.03 250));color: light-dark(oklch(0.25 0.03 250), oklch(0.97 0.02 250));}
light-dark() es Baseline 2024 (newly available): anda en Chrome, Firefox y Safari actuales, pero no en versiones de antes de 2024. Solo acepta colores: para una imagen distinta en oscuro seguí usando la media query. Además, color-scheme también cambia los colores del navegador: campos de formulario, barras de scroll y fondo de la página.
Elegí un esquema y mové el tono de la marca: la hoja de tokens cambia con light-dark(), y el botón y las cuatro muestras salen de un solo color. Las muestras se ubican en su luminosidad real, y las marcas naranjas muestran cuánto se movió cada una.
Tokens de color
Todo sale de --marca: solo cambia el tono.
- --marca
- oklch(0.6 0.15 250)
- --marca-clara
- oklch(0.85 0.15 250)
- --marca-oscura
- oklch(0.35 0.15 250)
- --marca-apagada
- oklch(0.6 0.05 250)
.tema {color-scheme: light;--marca: oklch(0.6 0.15 250);/* respaldo: valores calculados de antemano */--marca-clara: oklch(0.85 0.15 250);--marca-oscura: oklch(0.35 0.15 250);--marca-apagada: oklch(0.6 0.05 250);}@supports (color: oklch(from red l c h)) {.tema {--marca-clara: oklch(from var(--marca) calc(l + 0.25) c h);--marca-oscura: oklch(from var(--marca) calc(l - 0.25) c h);--marca-apagada: oklch(from var(--marca) l calc(c / 3) h);}}.tarjeta {background: oklch(0.97 0.02 250);color: oklch(0.25 0.03 250);}@supports (color: light-dark(white, black)) {.tarjeta {background: light-dark(oklch(0.97 0.02 250), oklch(0.25 0.03 250));color: light-dark(oklch(0.25 0.03 250), oklch(0.97 0.02 250));}}.boton {background: var(--marca);color: black; /* 5.3:1 */}@supports (color: contrast-color(red)) {.boton {color: contrast-color(var(--marca));}}
<div class="scheme-light [--marca:oklch(0.6_0.15_250)]"><div class="bg-[light-dark(oklch(0.97_0.02_250),oklch(0.25_0.03_250))] text-[light-dark(oklch(0.25_0.03_250),oklch(0.97_0.02_250))] rounded-lg p-4"><h3>Tokens de color</h3><div class="bg-[oklch(from_var(--marca)_calc(l_+_0.25)_c_h)] p-3"></div><div class="bg-[oklch(from_var(--marca)_calc(l_-_0.25)_c_h)] p-3"></div><div class="bg-[oklch(from_var(--marca)_l_calc(c_/_3)_h)] p-3"></div><button class="bg-(--marca) text-black supports-[color:contrast-color(red)]:text-[color:contrast-color(var(--marca))] rounded-md px-3 py-1.5">Guardar tema</button></div></div>
Colores relativos
La sintaxis de color relativo (oklch(from …)) toma un color de origen y lo separa en sus canales: l, c y h. Cada canal es un número que podés dejar igual o cambiar con calc(). Con --marca: oklch(0.6 0.15 250):
- Clara
calc(l + 0.25)oklch(from var(--marca) calc(l + 0.25) c h)sube la luminosidad a 0.85. - Oscura
calc(l - 0.25)La baja a 0.35. - Apagada
calc(c / 3)Deja el croma en 0.05, casi gris. - Transparente
l c h / 0.2oklch(from var(--marca) l c h / 0.2)agrega 20% de opacidad.
Si cambiás --marca, cambia todo el tema. Los colores relativos son Baseline 2024 (newly available). Para los navegadores que no los soportan, declará antes los valores ya calculados y poné los relativos dentro de @supports:
.tema {--marca-clara: oklch(0.85 0.15 250);}@supports (color: oklch(from red l c h)) {.tema {--marca-clara: oklch(from var(--marca) calc(l + 0.25) c h);}}
Contraste automático y gama amplia
contrast-color(var(--marca)) devuelve blanco o negro, el que más contraste tenga con ese color. Es Baseline 2026 (newly available), así que usalo como mejora progresiva: un color de texto fijo afuera y contrast-color() dentro de @supports, como en la demo.
Tiene límites: solo elige entre blanco y negro, y el que elige no siempre alcanza el contraste que necesitás. Con luminosidad 0.6 gana el negro en todos los tonos, con entre 4.9:1 y 6.1:1 según WCAG 2: alcanza AA (4.5:1) para texto normal, pero no AAA (7:1).
Revisá siempre el resultado con una herramienta de contraste. Para colores más saturados que sRGB, color(display-p3 …) pide colores de la gama P3 de las pantallas modernas. Tanto color() como la media query color-gamut son Baseline widely available. Si la pantalla no llega a P3, el navegador acerca el color al más parecido; con la media query elegís vos el respaldo:
.boton {background: rgb(255 0 128);}@media (color-gamut: p3) {.boton {background: color(display-p3 1 0 0.5);}}
En Tailwind
Tailwind v4 trae utilidades para color-scheme: scheme-light, scheme-dark y scheme-light-dark. Para light-dark(), los colores relativos y contrast-color() no hay utilidad propia: van como valores arbitrarios, con guiones bajos en lugar de espacios.
| CSS | Tailwind |
|---|---|
| color-scheme: light dark | scheme-light-dark |
| --marca: oklch(0.6 0.15 250) | [--marca:oklch(0.6_0.15_250)] |
| background-color: var(--marca) | bg-(--marca) |
| background-color: light-dark(white, black) | bg-[light-dark(white,black)] |
| oklch(from var(--marca) calc(l + 0.25) c h) | bg-[oklch(from_var(--marca)_calc(l+0.25)_c_h)] |
| @supports | supports-[…]: |
<div class="scheme-light-dark [--marca:oklch(0.6_0.15_250)]"><button class="bg-(--marca) text-blacksupports-[color:contrast-color(red)]:text-[color:contrast-color(var(--marca))]">Guardar</button></div>
Modern color
Lesson 34 of 37 · 4 min
With oklch, color-mix and custom properties you can already build a palette. CSS now adds functions that derive the rest of the theme from a single color, and pick between light and dark on their own.
One value for both themes
light-dark() takes two colors: it returns the first when the element uses the light scheme and the second when it uses the dark one. The color-scheme property decides. With color-scheme: light dark on :root, the browser follows the system preference, the same one @media (prefers-color-scheme: dark) reads. With color-scheme: dark you force dark on that element and its children. Without color-scheme, it always returns the light one.
:root {color-scheme: light dark;}body {background: light-dark(oklch(0.97 0.02 250), oklch(0.25 0.03 250));color: light-dark(oklch(0.25 0.03 250), oklch(0.97 0.02 250));}
light-dark() is Baseline 2024 (newly available): it works in current Chrome, Firefox and Safari, but not in versions from before 2024. It only takes colors: for a different image in dark mode, keep using the media query. On top of that, color-scheme also changes the browser's own colors: form fields, scrollbars and the page background.
Pick a scheme and move the brand hue: the token sheet changes with light-dark(), and the button and all four swatches come from a single color. The swatches sit at their real lightness, and the orange marks show how far each one moved.
Color tokens
Everything comes from --brand: only the hue changes.
- --brand
- oklch(0.6 0.15 250)
- --brand-tint
- oklch(0.85 0.15 250)
- --brand-shade
- oklch(0.35 0.15 250)
- --brand-muted
- oklch(0.6 0.05 250)
.theme {color-scheme: light;--brand: oklch(0.6 0.15 250);/* fallback: values computed ahead of time */--brand-tint: oklch(0.85 0.15 250);--brand-shade: oklch(0.35 0.15 250);--brand-muted: oklch(0.6 0.05 250);}@supports (color: oklch(from red l c h)) {.theme {--brand-tint: oklch(from var(--brand) calc(l + 0.25) c h);--brand-shade: oklch(from var(--brand) calc(l - 0.25) c h);--brand-muted: oklch(from var(--brand) l calc(c / 3) h);}}.card {background: oklch(0.97 0.02 250);color: oklch(0.25 0.03 250);}@supports (color: light-dark(white, black)) {.card {background: light-dark(oklch(0.97 0.02 250), oklch(0.25 0.03 250));color: light-dark(oklch(0.25 0.03 250), oklch(0.97 0.02 250));}}.button {background: var(--brand);color: black; /* 5.3:1 */}@supports (color: contrast-color(red)) {.button {color: contrast-color(var(--brand));}}
<div class="scheme-light [--brand:oklch(0.6_0.15_250)]"><div class="bg-[light-dark(oklch(0.97_0.02_250),oklch(0.25_0.03_250))] text-[light-dark(oklch(0.25_0.03_250),oklch(0.97_0.02_250))] rounded-lg p-4"><h3>Color tokens</h3><div class="bg-[oklch(from_var(--brand)_calc(l_+_0.25)_c_h)] p-3"></div><div class="bg-[oklch(from_var(--brand)_calc(l_-_0.25)_c_h)] p-3"></div><div class="bg-[oklch(from_var(--brand)_l_calc(c_/_3)_h)] p-3"></div><button class="bg-(--brand) text-black supports-[color:contrast-color(red)]:text-[color:contrast-color(var(--brand))] rounded-md px-3 py-1.5">Save theme</button></div></div>
Relative colors
Relative color syntax (oklch(from …)) takes an origin color and splits it into its channels: l, c and h. Each channel is a number you can keep or change with calc(). With --brand: oklch(0.6 0.15 250):
- Tint
calc(l + 0.25)oklch(from var(--brand) calc(l + 0.25) c h)raises the lightness to 0.85. - Shade
calc(l - 0.25)Lowers it to 0.35. - Muted
calc(c / 3)Leaves the chroma at 0.05, almost gray. - Transparent
l c h / 0.2oklch(from var(--brand) l c h / 0.2)adds 20% opacity.
Change --brand and the whole theme changes. Relative colors are Baseline 2024 (newly available). For browsers without them, declare the precomputed values first and put the relative ones inside @supports:
.theme {--brand-tint: oklch(0.85 0.15 250);}@supports (color: oklch(from red l c h)) {.theme {--brand-tint: oklch(from var(--brand) calc(l + 0.25) c h);}}
Automatic contrast and wide gamut
contrast-color(var(--brand)) returns white or black, whichever contrasts more with that color. It's Baseline 2026 (newly available), so use it as progressive enhancement: a fixed text color outside and contrast-color() inside @supports, as in the demo.
It has limits: it only picks between white and black, and the one it picks doesn't always reach the contrast you need. At lightness 0.6 black wins for every hue, with between 4.9:1 and 6.1:1 by WCAG 2: enough for AA (4.5:1) on body text, but not AAA (7:1).
Always check the result with a contrast tool. For colors more saturated than sRGB, color(display-p3 …) asks for colors in the P3 gamut of modern screens. Both color() and the color-gamut media query are Baseline widely available. If the screen can't show P3, the browser maps the color to the closest one; with the media query you choose the fallback yourself:
.button {background: rgb(255 0 128);}@media (color-gamut: p3) {.button {background: color(display-p3 1 0 0.5);}}
In Tailwind
Tailwind v4 ships utilities for color-scheme: scheme-light, scheme-dark and scheme-light-dark. For light-dark(), relative colors and contrast-color() there's no dedicated utility: they go in as arbitrary values, with underscores instead of spaces.
| CSS | Tailwind |
|---|---|
| color-scheme: light dark | scheme-light-dark |
| --brand: oklch(0.6 0.15 250) | [--brand:oklch(0.6_0.15_250)] |
| background-color: var(--brand) | bg-(--brand) |
| background-color: light-dark(white, black) | bg-[light-dark(white,black)] |
| oklch(from var(--brand) calc(l + 0.25) c h) | bg-[oklch(from_var(--brand)_calc(l+0.25)_c_h)] |
| @supports | supports-[…]: |
<div class="scheme-light-dark [--brand:oklch(0.6_0.15_250)]"><button class="bg-(--brand) text-blacksupports-[color:contrast-color(red)]:text-[color:contrast-color(var(--brand))]">Save</button></div>
