La versión corta
- Un tema con variables CSS funciona mejor en dos capas: variables de paleta que guardan los colores en bruto y variables semánticas que describen para qué sirve cada color.
- Los componentes solo deberían leer variables semánticas como --color-text o --color-surface, nunca valores de paleta directamente.
- El modo oscuro consiste entonces en apuntar las variables semánticas a otros valores de la paleta, dentro de una media query o un atributo data.
- La función light-dark() y la propiedad color-scheme permiten que una sola declaración contenga los dos modos en los navegadores actuales.
- Un pequeño script en línea en el head que aplica el tema guardado antes de que se pinte la página evita el parpadeo del tema equivocado.
En esta página
Un tema con variables CSS es un conjunto de propiedades personalizadas, como --color-surface y --color-text, que todos los componentes leen en lugar de colores escritos a mano. Para cambiar el tema cambias las variables, normalmente en :root o en un atributo data-theme, y toda la interfaz lo sigue. El truco que lo hace escalar es dividir las variables en dos capas: una paleta de colores en bruto y unos roles semánticos que apuntan a esa paleta.
Dos capas: paleta y roles
Las variables de paleta se nombran por lo que son: --violet-600, --ink-900. Las variables semánticas se nombran por lo que hacen: --color-accent, --color-text-muted. Los componentes solo leen las segundas. Cuando llega el modo oscuro, reapuntas los roles y no tocas ningún componente. Es la misma idea que los tokens de diseño, expresada directamente en CSS.
--violet-600: #7c3aedQuién la lee
Cambia cuando
--color-accent: var(--violet-600)Quién la lee
Cambia cuando
Ejemplo
--button-bg: var(--color-accent)Quién la lee
Cambia cuando
:root {
/* Palette: raw values, named for what they are */
--paper-50: #faf8f5;
--paper-100: #f1ede6;
--ink-900: #14121f;
--ink-700: #3d3a4f;
--ink-500: #6b6880;
--violet-600: #6d28d9;
--violet-300: #c4b5fd;
--night-950: #0d0b16;
--night-900: #17142a;
--night-800: #221e3a;
/* Roles: what each colour is for (light mode) */
--color-bg: var(--paper-50);
--color-surface: var(--paper-100);
--color-text: var(--ink-900);
--color-text-muted: var(--ink-500);
--color-border: color-mix(in oklab, var(--ink-900) 12%, transparent);
--color-accent: var(--violet-600);
--color-on-accent: #ffffff;
}
body { background: var(--color-bg); color: var(--color-text); }
.card { background: var(--color-surface); border: 1px solid var(--color-border); }
.button { background: var(--color-accent); color: var(--color-on-accent); }Nombres de rol que sobreviven a un rediseño
Un nombre de rol debería seguir siendo cierto después de que cambien los colores. --light-grey deja de ser claro en modo oscuro, y --blue miente el día que la marca se vuelve verde. Nombra la función y la pareja, para que quien lea la hoja de estilos después sepa qué color va sobre cuál.
Nombres que se rompen
--blue,--purple-button--light-grey-bg--white-text--dark-border
Nombres que duran
--color-accent,--color-on-accent--color-surface--color-text--color-border
Añadir modo oscuro con variables CSS
El modo oscuro reapunta los roles. Respeta por defecto el ajuste del sistema operativo y deja que un atributo data-theme lo sobrescriba cuando el lector elige. Define también color-scheme, para que las barras de desplazamiento, los controles de formulario y el lienzo por defecto coincidan.
:root { color-scheme: light; }
/* Follow the system unless the reader picked light */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
color-scheme: dark;
--color-bg: var(--night-950);
--color-surface: var(--night-900);
--color-text: #f2f0fa;
--color-text-muted: #a5a1bd;
--color-border: color-mix(in oklab, #ffffff 12%, transparent);
--color-accent: var(--violet-300);
--color-on-accent: var(--ink-900);
}
}
/* The reader picked dark explicitly */
:root[data-theme="dark"] {
color-scheme: dark;
--color-bg: var(--night-950);
--color-surface: var(--night-900);
--color-text: #f2f0fa;
--color-text-muted: #a5a1bd;
--color-border: color-mix(in oklab, #ffffff 12%, transparent);
--color-accent: var(--violet-300);
--color-on-accent: var(--ink-900);
}light-dark() elimina la repetición.El modo oscuro no es el modo claro invertido. Las superficies se aclaran a medida que suben, los acentos se aclaran y las sombras dejan paso a los bordes. Diseño en modo oscuro y degradados para modo oscuro cubren la parte visual.
El atajo light-dark()
Los navegadores actuales admiten light-dark(), que recibe un valor claro y uno oscuro y elige uno según el color-scheme del elemento. Define color-scheme: light dark en la raíz para seguir al sistema, o fuerza uno con el atributo, y cada rol se queda en una sola línea: --color-bg: light-dark(var(--paper-50), var(--night-950)). Mantén la forma larga si tienes que dar soporte a navegadores anteriores a 2024.
Un selector de tema que no parpadea
Si la elección guardada se aplica después de que se renderice la página, los lectores que eligieron el modo oscuro ven un destello blanco en cada carga. Aplica el atributo en un pequeño script en línea dentro del head, antes de que ninguna hoja de estilos pinte el body.
<head>
<script>
(function () {
var saved = localStorage.getItem("theme");
if (saved === "light" || saved === "dark") {
document.documentElement.dataset.theme = saved;
}
})();
</script>
<link rel="stylesheet" href="/styles.css">
</head>dataset.theme y guardar el valor en localStorage.- 1
Ofrece tres opciones
Claro, oscuro y sistema. Quitar el atributo devuelve el control a
prefers-color-scheme. - 2
Guarda solo las elecciones explícitas
Guarda
lightodark; borra la clave para sistema, de modo que se respete un cambio posterior en el sistema operativo. - 3
Evita transiciones al cargar
Si los colores tienen transición, actívala solo después del primer pintado, o la página se animará de claro a oscuro mientras carga.
Temas de marca más allá del claro y el oscuro
Las mismas dos capas sirven para varias marcas en un mismo código. Cada marca aporta su propia paleta y sus valores de rol bajo un atributo data-brand, y los modos siguen funcionando encima. Con color-mix() puedes derivar estados hover y tintes a partir de un acento por marca, como se muestra en color-mix() en CSS.
[data-brand="harbour"] {
--color-accent: #0f766e;
--gradient-hero: linear-gradient(150deg, #042f2e 0%, #0f766e 55%, #5eead4 100%);
}
[data-brand="ember"] {
--color-accent: #c2410c;
--gradient-hero: linear-gradient(150deg, #431407 0%, #c2410c 55%, #fdba74 100%);
}
.button:hover { background: color-mix(in oklab, var(--color-accent), black 12%); }
.hero { background: var(--gradient-hero); }Como las propiedades personalizadas se heredan, un tema no tiene por qué cubrir toda la página. Pon data-theme="dark" en una sección, como una franja promocional o un pie de página, y todo lo que haya dentro leerá los roles oscuros mientras el resto de la página sigue claro. Escribe el selector oscuro como [data-theme="dark"] en lugar de :root[data-theme="dark"] si quieres que funcione, y define también color-scheme en la sección.
Mantén la web y tus diseños en una sola paleta
Un tema en CSS es solo la mitad de una marca. Los mismos colores deberían aparecer en tus posts, presentaciones y emails. El kit de marca de Gradiently guarda tus paletas, tus logos y tus fuentes de títulos y de texto, y el Designer lo aplica cuando maqueta un diseño, así que el post que haces el lunes usa el mismo acento que tu web. Si todavía estás eligiendo esos colores, empieza por cómo elegir los colores de marca.
Preguntas frecuentes
¿Cómo creo un tema con variables CSS?
Define variables de paleta con los colores en bruto y después variables semánticas como --color-bg que apunten a ellas. Los componentes solo leen las semánticas, y los temas las reapuntan.
¿Cómo añado modo oscuro con variables CSS?
Sobrescribe las variables semánticas dentro de @media (prefers-color-scheme: dark) y bajo un selector [data-theme="dark"] para los lectores que lo elijan.
¿Cómo evito que el tema parpadee al cargar la página?
Lee el tema guardado en un pequeño script en línea dentro del head y aplica el atributo antes de que la hoja de estilos pinte la página.
¿Qué hace light-dark() en CSS?
Devuelve su primer valor en modo claro y el segundo en modo oscuro, según el color-scheme del elemento. Funciona en los navegadores principales actuales.
¿Puedo guardar un degradado en una variable CSS?
Sí. Guarda el degradado completo como valor y úsalo en background. Para animar entre degradados, registra las paradas con @property.
Escrito por Gradiently
El equipo detrás de Gradiently, una herramienta de diseño creada en torno a los Marks: degradados vivos que hacen que todo lo que diseñas parezca tuyo.
Ver nuestro perfil