Back

Creación de un Selector de Temas con Variables CSS

Creación de un Selector de Temas con Variables CSS

Los valores de color codificados de forma rígida y dispersos por una hoja de estilos hacen que cambiar de tema sea una tarea tediosa. Cambias un color y terminas rastreando docenas de declaraciones. Las variables CSS resuelven esto de forma elegante: define tus design tokens una sola vez y luego cambia de tema modificando un único atributo en el elemento raíz.

Este artículo recorre la construcción de un selector de temas mantenible: uno que respeta automáticamente las preferencias del sistema, responde a la elección manual del usuario y conserva esa elección entre sesiones.

Puntos Clave

  • Las propiedades personalizadas de CSS son dinámicas y se actualizan en tiempo de ejecución, lo que las convierte en design tokens ideales para la gestión de temas.
  • Un atributo data-theme en el elemento raíz proporciona una forma limpia y escalable de alternar entre cualquier número de temas.
  • La media query prefers-color-scheme gestiona los valores predeterminados del sistema, mientras que JavaScript y localStorage preservan las elecciones manuales del usuario.
  • Un pequeño script inline en el <head> previene el parpadeo del tema incorrecto al cargar la página.
  • Incluir la propiedad color-scheme garantiza que la UI nativa del navegador se adapte para coincidir con el tema activo.

Por Qué las Variables CSS Son la Base Adecuada para la Gestión de Temas

Las propiedades personalizadas de CSS son dinámicas. A diferencia de las variables de Sass, que se compilan a valores estáticos, las variables CSS viven en el navegador y pueden actualizarse en tiempo de ejecución, ya sea mediante media queries, JavaScript o un elemento padre que cambia de estado.

Eso las convierte en design tokens ideales. Define tus colores de superficie, colores de texto y colores de acento como variables en :root, y luego referencia esas variables a lo largo de tu hoja de estilos. Cambiar de tema se reduce a un único cambio en el DOM, en lugar de intercambiar hojas de estilo.

Configurando tus Tokens de Tema

Comienza definiendo tu tema claro como predeterminado y luego declara una variante oscura usando un selector de atributo data-theme:

:root {
  color-scheme: light;
  --color-bg: #ffffff;
  --color-text: #1a1a1a;
  --color-primary: #302ae6;
  --color-surface: #f4f4f4;
}

[data-theme="dark"] {
  color-scheme: dark;
  --color-bg: #161625;
  --color-text: #e1e1ff;
  --color-primary: #9a97f3;
  --color-surface: #1e1e30;
}

Vale la pena incluir la propiedad color-scheme. Le indica al navegador qué modo está activo, de modo que los elementos nativos de la UI —barras de desplazamiento, inputs de formulario, anillos de foco— se adaptan automáticamente para coincidir con tu tema.

Ahora utiliza esos tokens en toda tu hoja de estilos:

body {
  background-color: var(--color-bg);
  color: var(--color-text);
}

Respetando las Preferencias del Sistema con prefers-color-scheme

Las preferencias del sistema y el cambio manual de tema resuelven problemas distintos. prefers-color-scheme gestiona el caso automático: usuarios que han configurado su sistema operativo en modo oscuro y esperan que los sitios web sigan esa preferencia:

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --color-bg: #161625;
    --color-text: #e1e1ff;
    --color-primary: #9a97f3;
    --color-surface: #1e1e30;
  }
}

La protección :not([data-theme="light"]) garantiza que la elección explícita del usuario prevalezca sobre el valor predeterminado del sistema. Sin ella, la media query anularía la selección manual.

Una Nota sobre light-dark()

La función CSS light-dark() permite declarar ambos valores del tema de forma inline:

:root {
  color-scheme: light dark;
  --color-bg: light-dark(#ffffff, #161625);
}

Resulta elegante para conjuntos de tokens más simples y funciona bien cuando solo necesitas dos temas. Para sistemas con más de dos temas o tokens que varían más allá del color, el enfoque del selector de atributos te brinda mayor control.

Ten en cuenta que light-dark() requiere que color-scheme se establezca con ambos valores para que la función se resuelva correctamente. El soporte en navegadores modernos es sólido, pero aún relativamente reciente (Chrome 123+, Safari 17.5+, Firefox 120+), así que confirma la compatibilidad con tu audiencia antes de depender de ella.

Añadiendo JavaScript para un Control Manual Persistente

La detección de las preferencias del sistema es puramente CSS, pero recordar la elección manual del usuario requiere JavaScript. Aquí tienes una implementación mínima y lista para producción:

const STORAGE_KEY = 'theme-preference';
const root = document.documentElement;

function getSystemTheme() {
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}

function getSavedTheme() {
  try {
    return localStorage.getItem(STORAGE_KEY);
  } catch {
    return null;
  }
}

function applyTheme(theme) {
  root.setAttribute('data-theme', theme);

  try {
    localStorage.setItem(STORAGE_KEY, theme);
  } catch {
    // Storage unavailable — theme still applies for this session
  }
}

// On load: respect saved preference, fall back to system
applyTheme(getSavedTheme() || getSystemTheme());

// Wire up your toggle button
document.getElementById('theme-toggle').addEventListener('click', () => {
  const current = root.getAttribute('data-theme');
  applyTheme(current === 'dark' ? 'light' : 'dark');
});

// Sync with OS changes when no manual preference is set
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
  if (!getSavedTheme()) {
    root.setAttribute('data-theme', e.matches ? 'dark' : 'light');
  }
});

Los bloques try/catch alrededor de localStorage manejan con elegancia los modos de navegación privada y los errores de cuota de almacenamiento: el tema sigue aplicándose en la sesión actual incluso si no puede guardarse.

Previniendo el Parpadeo del Tema Incorrecto

Si tu JavaScript se carga después de que la página se renderiza, los usuarios verán brevemente el tema predeterminado antes de que se aplique el correcto. Soluciona esto con un pequeño script inline bloqueante en el <head>:

<script>
  try {
    const saved = localStorage.getItem('theme-preference');

    if (saved) {
      document.documentElement.setAttribute('data-theme', saved);
    } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
      document.documentElement.setAttribute('data-theme', 'dark');
    }
  } catch {
    if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
      document.documentElement.setAttribute('data-theme', 'dark');
    }
  }
</script>

Este script se ejecuta de forma síncrona antes de que cualquier contenido se renderice, eliminando el parpadeo. Colócalo lo más temprano posible en el <head>, antes de cualquier hoja de estilos, de modo que el atributo ya exista cuando el CSS se aplique por primera vez.

El Patrón que Escala

Las variables CSS te brindan una separación limpia entre tus design tokens y los estilos de tus componentes. Los componentes referencian variables; los temas definen variables. Añadir un tercer tema —alto contraste, una variante de marca, una paleta estacional— significa agregar un nuevo bloque de selector de atributo en CSS y una nueva opción en la lógica de tu selector. Nada más cambia.

Conclusión

El verdadero valor aquí no es solamente el modo oscuro: es una arquitectura de temas que se mantiene escalable a medida que tu sistema de diseño crece. Al tratar las variables CSS como design tokens, respetar tanto las preferencias del sistema como las del usuario, y gestionar los casos límite (fallos de almacenamiento, parpadeos de renderizado, cambios a nivel de sistema operativo), construyes un selector que se siente nativo a la plataforma y resulta trivial de extender. El patrón es pequeño, pero la rentabilidad se acumula con cada nuevo tema o componente que añades.

Preguntas Frecuentes

Ambos funcionan, pero un atributo data-theme suele ser más limpio porque expresa un único valor en lugar de una lista de modificadores. Las clases son más adecuadas para estados composicionales donde coexisten múltiples flags. Con los temas, normalmente tienes un único valor activo a la vez, lo cual encaja de forma más natural con la semántica de un selector de atributo.

Ese parpadeo ocurre cuando tu JavaScript se ejecuta después de que el navegador ya ha pintado la página utilizando los estilos predeterminados. La solución es un pequeño script inline síncrono en el head que lea la preferencia guardada desde localStorage y establezca el atributo data-theme antes de que ocurra cualquier renderizado. Para visitantes que llegan por primera vez, también puedes verificar prefers-color-scheme para que el renderizado inicial coincida de inmediato con el tema del sistema.

Sí. Las variables CSS pueden contener cualquier valor CSS válido, incluyendo escalas de espaciado, radios de borde, sombras, pilas de fuentes y duraciones de animación. Esto las hace útiles para gestionar densidad (diseños compactos versus cómodos), preferencias de movimiento o incluso variantes tipográficas. Trata las variables como design tokens de forma amplia, no solo como referencias de color.

Funciona bien en navegadores modernos (Chrome 123+, Safari 17.5+, Firefox 120+) pero carece de soporte en versiones más antiguas. Para proyectos con una audiencia moderna, es una opción limpia. Para una mayor compatibilidad, el enfoque del atributo data-theme sigue siendo más seguro y además escala a más de dos temas, algo que light-dark() no puede hacer por sí sola.

Truly understand users experience

See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data. . Check our GitHub repo and join the thousands of developers in our community..

OpenReplay