Los temas oscuros añadidos a posteriori dejan un rastro reconocible: una variable aquí, un !important allá, un fichero dark.css duplicado que diverge del tema claro sprint tras sprint. El problema no es el color, es la ausencia de estructura. Los design tokens expresados como propiedades personalizadas CSS aportan esa estructura, sin dependencia ni capa JavaScript.
Un token no es un color
La confusión más extendida consiste en asimilar un valor bruto (#1e40af) a un token. Un design token es una intención con nombre: «el color del texto principal», no «un azul oscuro». De ahí una organización en dos capas. La capa primitiva contiene los valores brutos, nunca usados directamente por los componentes. La capa semántica contiene las intenciones, y es la única que tocan los selectores.
:root {
/* Capa 1: primitivas (nunca usadas tal cual) */
--gray-900: #111418;
--gray-50: #f7f8fa;
--blue-500: #2563eb;
--blue-300: #93c5fd;
/* Capa 2: semántica (la API de los componentes) */
--color-bg: var(--gray-50);
--color-text: var(--gray-900);
--color-accent: var(--blue-500);
}El beneficio en mantenimiento es inmediato: un cambio de tono de marca se trata en las primitivas, un problema de contraste detectado en una auditoría de accesibilidad se trata en la capa semántica. Los componentes no se mueven: consumen var(--color-text).
Un componente que referencia un color bruto es deuda técnica que se ignora. Un componente que referencia un token semántico es un contrato.
El modo oscuro como redefinición
Una vez establecida esa separación, el modo oscuro se convierte en una operación sin relieve, que es exactamente el objetivo. No se duplica nada: solo se redefine la capa semántica. Las primitivas permanecen, los componentes permanecen, solo cambia la tabla de correspondencia.
1. Respetar la preferencia del sistema
El punto de partida es prefers-color-scheme, que traduce la preferencia expresada por el usuario en su sistema operativo. Ignorarla equivale a pasar por encima de un ajuste explícito.
@media (prefers-color-scheme: dark) {
:root {
--color-bg: var(--gray-900);
--color-text: var(--gray-50);
--color-accent: var(--blue-300);
}
}2. Permitir una elección explícita
Un atributo colocado en el elemento raíz permite anular la preferencia del sistema cuando el usuario cambia manualmente. El orden de las reglas determina la prioridad: la elección explícita debe imponerse en ambos sentidos.
:root[data-theme='dark'] { /* mismas redefiniciones */ }
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) { /* mismas redefiniciones */ }
}3. Evitar el destello al cargar
La única línea de JavaScript realmente necesaria es un script bloqueante situado en el <head>, que aplica el tema memorizado antes del primer renderizado. Cualquier otro enfoque produce un destello de tema claro en las páginas cargadas en modo oscuro.
<script>
try {
var t = localStorage.getItem('theme');
if (t) document.documentElement.setAttribute('data-theme', t);
} catch (e) {}
</script>color-scheme: light u dark en :root no es un detalle. Es lo que indica al navegador que adapte los controles de formulario, las barras de desplazamiento y los campos nativos. Sin esa declaración, un tema oscuro siempre queda delatado por sus <select>.Lo que el método evita
| Síntoma frecuente | Causa | Respuesta con tokens |
|---|---|---|
Fichero dark.css que diverge | Duplicación de reglas | Una sola hoja, redefinición semántica |
Acumulación de !important | Conflictos de especificidad | Los componentes solo leen una variable |
| Contraste insuficiente en oscuro | Colores elegidos caso por caso | Corrección centralizada en un token |
| Destello claro al cargar | Tema aplicado tras el renderizado | Script bloqueante en el <head> |
Lo que hay que retener
Un tema claro/oscuro mantenible no exige ni framework ni capa JavaScript: dos capas de tokens, una redefinición semántica y un script de tres líneas contra el destello inicial. Lo esencial del trabajo es una decisión de arquitectura tomada antes de escribir la primera regla de estilo.
Es el método que aplico en mis proyectos de cliente desde hace varios años, y el tema de este sitio es una aplicación directa. La regla que ya no negocio: ningún componente referencia una primitiva. El día que esa regla salta, la deuda vuelve en seis meses. — Simon Janvier
