Cómo implementar modo oscuro en tu web con variables CSS

Última actualización: 28/01/2026
Autor: Isaac
  • Usar variables CSS en :root permite definir una paleta común y alternar entre modo claro y oscuro modificando solo esos valores.
  • La combinación de prefers-color-scheme, clases o atributos en html/body y un interruptor con JavaScript ofrece control automático y manual.
  • Almacenar la elección en localStorage garantiza que la web recuerde el tema elegido por el usuario en futuras visitas.
  • Cuidar contraste, colores de acento y accesibilidad convierte el modo oscuro en una mejora real de la experiencia, no solo en un efecto visual.

css

Si llevas un tiempo trasteando con front-end, seguro que te has cruzado con el famoso modo oscuro en interfaces web. No es solo una moda: a la gente le gusta, relaja la vista por la noche, queda moderno y, si se hace bien, mejora incluso la accesibilidad. La parte buena es que no hace falta montar un lío tremendo: con variables CSS, media queries y un poco de JavaScript opcional puedes tener un sistema de temas muy sólido.

En las próximas líneas vas a ver distintas formas de implementar un modo oscuro en una web usando variables CSS: desde soluciones sin JavaScript y trucos con :has(), hasta configuraciones completas con persistencia en localStorage y soporte para prefers-color-scheme. La idea es que entiendas bien las piezas, para que luego puedas combinarlas a tu gusto y no limitarte a copiar y pegar.

Por qué merece la pena ofrecer modo oscuro

Más allá de que «queda chulo», el dark mode tiene varias ventajas claras. Por un lado, reduce el deslumbramiento en entornos con poca luz, lo que ayuda a muchas personas a leer más cómodo durante más tiempo. Por otro, en pantallas OLED y similares, los píxeles negros se apagan y eso se traduce en ahorro de batería, algo que tus usuarios móviles agradecen.

También hay un componente de accesibilidad y personalización: muchos sistemas operativos ya permiten elegir tema claro u oscuro a nivel global, e incluso expresan esa preferencia a través de CSS con prefers-color-scheme. Si tu sitio respeta esa elección, estás dando una experiencia más coherente, cuidando a las personas con sensibilidad a la luz y sumando puntos en usabilidad.

Fundamentos: variables CSS como base de los temas

La pieza clave para que todo esto no se convierta en un infierno de estilos duplicados son las propiedades personalizadas de CSS, es decir, las famosas variables CSS. Funcionan como contenedores de valores que luego reutilizas por todo el documento con var(). Si organizas bien estas variables, cambiar de tema es tan simple como reasignar unos cuantos valores.

Lo habitual es definir el set de variables globales en :root, que apunta al elemento <html>. Desde ahí, todo el documento hereda esos colores. Un ejemplo simplificado para un tema claro podría ser:

:root {
  --color-fondo: #ffffff;
  --color-texto: #222222;
  --color-acento: #007bff;
}

body {
  background: var(--color-fondo);
  color: var(--color-texto);
}

a, button {
  color: var(--color-acento);
}

En cuanto quieras un tema oscuro, en lugar de reescribir todos los selectores, solo redefinirás estas variables bajo alguna condición (una clase, un atributo, una media query, un selector con :has()…). Por eso es tan importante que te acostumbres a usar variables en todo lo que pueda cambiar de un tema a otro: fondos, textos, bordes, sombras, incluso tipografías o imágenes.

Modo oscuro solo con CSS usando :has() y un checkbox

Existe una forma muy curiosa de activar un tema oscuro sin tocar JavaScript, aprovechando el selector relacional :has(). La idea es combinarlo con un checkbox que actúe como interruptor y redefinir las variables cuando ese checkbox está marcado. Es una solución ideal para páginas de una sola vista, demos o pequeños experimentos, porque no ofrece persistencia entre páginas.

El flujo es este: primero defines tus colores por defecto en :root, luego haces que el body use esas variables, y después utilizas body:has(#darkmode-toggle:checked) para cambiar los valores cuando la casilla está activada. El CSS podría ser algo así:

/* Colores por defecto (modo claro) */
:root {
  --bg-color: #ffffff;
  --text-color: #222222;
}

body {
  background: var(--bg-color);
  color: var(--text-color);
  transition: background 0.3s, color 0.3s;
}

/* Cuando el checkbox esté marcado, usamos los colores oscuros */
body:has(#darkmode-toggle:checked) {
  --bg-color: #1e1e1e;
  --text-color: #f5f5f5;
}

En el HTML solo necesitas colocar el checkbox en algún punto dentro de <body> y referenciarlo con el mismo id que uses en el selector:

<input type="checkbox" id="darkmode-toggle">
<label for="darkmode-toggle">Modo oscuro</label>

Lo bueno de este método es que no estás atado a ninguna estructura concreta: el checkbox puede estar en el header, en un sidebar, donde quieras, mientras viva dentro del body. Lo malo es que, al no haber JavaScript ni almacenamiento, en cuanto recargues la página o cambies de URL, el tema se pierde y vuelves al estado inicial. Para una web multipágina esto se queda corto, pero como truco de solo CSS es muy limpio.

  5 Mejores Programas Para Dibujar Comics

Aprovechando prefers-color-scheme para seguir al sistema

Otra pata importante del modo oscuro moderno es dejar que el propio navegador y el sistema operativo indiquen la preferencia del usuario. Para eso existe la media query prefers-color-scheme, que te permite aplicar estilos distintos según si la persona tiene configurado tema claro u oscuro en su dispositivo.

Hay dos formas muy comunes de usar esta media query. Una es definir dos bloques separados, uno para claro y otro para oscuro, reasignando las variables en cada caso:

/* Valores por defecto para el modo claro */
:root {
  --body-bg: #ffffff;
  --body-color: #000000;
}

/* Si el usuario prefiere modo oscuro */
@media (prefers-color-scheme: dark) {
  :root {
    --body-bg: #000000;
    --body-color: #ffffff;
  }
}

Otra alternativa es la inversa: definir el modo oscuro solo dentro de la media query y dejar que el resto de valores actúen como «modo claro» por defecto, incluso para posibles valores futuros que añada la especificación:

/* Tema claro por defecto */
:root {
  --body-bg: #ffffff;
  --body-color: #000000;
}

/* Tema oscuro si el sistema lo indica */
@media (prefers-color-scheme: dark) {
  :root {
    --body-bg: #000000;
    --body-color: #ffffff;
  }
}

Luego, simplemente usas esas variables en tu hoja de estilos, por ejemplo para el cuerpo del documento:

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

Con esto consigues que tu sitio se adapte solo a la configuración del sistema. Si la persona cambia a modo oscuro en su móvil o en el escritorio, el navegador ajustará automáticamente tus variables; ten en cuenta las diferencias entre navegadores como Samsung Internet y Chrome. Eso sí, aquí todavía no hay interruptor manual ni persistencia propia del sitio: solo estás «obedeciendo» al sistema.

Montar un interruptor de tema con HTML, CSS y JavaScript

Si quieres ir un paso más allá y dar control al usuario, lo típico es añadir un botón o un toggle que permita alternar manualmente entre tema claro y oscuro. El HTML puede ser tan sencillo como un checkbox dentro de una etiqueta label, o un par de botones con iconos de sol y luna.

Por ejemplo, un patrón muy extendido es usar un checkbox con aspecto de interruptor:

<header>
  <div class="switch-wrapper" id="switch">
    <label>
      <input type="checkbox">
      <span class="slider"></span>
    </label>
  </div>
  <h1>Mi sitio con modo oscuro</h1>
</header>

El slider se maqueta con CSS para que se vea como un botón on/off, pero lo importante es el input. En CSS defines tus variables para ambos temas y las aplicas a los elementos, y luego usas JavaScript para cambiar la clase o atributo del elemento raíz cuando cambie el estado del checkbox.

Por ejemplo, usando un atributo personalizado en html llamado tema:

/* Tema claro por defecto */
:root {
  --color-fondo: #ffffff;
  --color-texto: #222222;
}

/* Tema oscuro */
:root {
  --color-fondo: #121212;
  --color-texto: #e0e0e0;
}

body {
  background-color: var(--color-fondo);
  color: var(--color-texto);
}

Y el JavaScript que reacciona al cambio del interruptor podría ser este:

const colorSwitch = document.querySelector('#switch input');

function cambiaTema(ev) {
  if (ev.target.checked) {
    document.documentElement.setAttribute('tema', 'dark');
  } else {
    document.documentElement.setAttribute('tema', 'light');
  }
}

colorSwitch.addEventListener('change', cambiaTema);

En cuanto el usuario haga clic, se actualiza el atributo del elemento raíz y, gracias a las variables CSS, toda la web cambia de aspecto sin que tengas que tocar más estilos. Este planteamiento es muy flexible: puedes añadir más temas (neón, sepia, alto contraste…) simplemente añadiendo nuevos valores y selectores.

Interruptor con clases, más temas y JavaScript mínimo

Otra variante muy parecida es trabajar con clases en lugar de atributos, algo más familiar si ya usas utilidades o frameworks. Por ejemplo, defines en tu CSS:

:root.light {
  --bg-color: #ffffff;
  --text-color: #222222;
}

:root.dark {
  --bg-color: #121212;
  --text-color: #e0e0e0;
}

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

A partir de ahí, con una sola línea de JavaScript puedes controlar el tema, asignando la clase adecuada al elemento raíz. Una función típica sería:

function setTheme(theme) {
  document.documentElement.className = theme;
}

Luego solo tienes que invocar esa función desde tus controles. Por ejemplo, con dos botones:

<button onclick="setTheme('light')" title="Usar tema claro">☀️</button>
<button onclick="setTheme('dark')" title="Usar tema oscuro">🌙</button>

Si quieres añadir más esquemas de color, basta con declarar nuevas clases en :root (por ejemplo .neon, .sepia, etc.) redefiniendo el conjunto de variables, y pasar esos valores a setTheme. Gracias a que toda la maquetación tira de var(), no tienes que cambiar nada más.

  Cómo usar Multiple GPU Rendering en apps compatibles

Selector de múltiples temas con <select> y almacenamiento local

Cuando quieres algo más organizado, sobre todo si ofreces varias paletas, un elemento <select> encaja muy bien. La idea es que cada opción tenga como value el nombre de la clase o el identificador del tema, y que JavaScript se limite a aplicar esa clase al <html> o al <body>.

Un HTML de partida podría ser:

<label for="theme-select">Tema de color:</label>
<select id="theme-select">
  <option value="theme-auto">Automático</option>
  <option value="theme-light">Claro</option>
  <option value="theme-dark">Oscuro</option>
</select>

En CSS defines la configuración base (por ejemplo, el modo claro) y luego creas las variantes como clases sobre el body o el html:

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

body.theme-dark {
  --bg-color: #121212;
  --text-color: #e0e0e0;
}

body.theme-auto {
  /* valores por defecto; se ajustarán con media queries */
}

Para la opción automática, puedes combinarlo con prefers-color-scheme de esta forma:

@media (prefers-color-scheme: dark) {
  body.theme-auto {
    --bg-color: #121212;
    --text-color: #e0e0e0;
  }
}

Es decir, cuando el sistema indique que el tema predeterminado es oscuro y el usuario tenga seleccionado «Automático», aplicarás colores oscuros sin forzar nada. Si el sistema está en claro o en un valor futuro, el cuerpo se quedará con la paleta por defecto.

El JavaScript para enlazar el desplegable con el tema y recordar la elección podría ser algo así:

document.addEventListener('DOMContentLoaded', () => {
  const select = document.getElementById('theme-select');
  const savedTheme = localStorage.getItem('theme') || 'theme-auto';

  document.body.classList.add(savedTheme);

  Array.from(select.options).forEach(option => {
    if (option.value === savedTheme) {
      option.selected = true;
    }
  });

  select.addEventListener('change', function () {
    document.body.classList.remove('theme-auto', 'theme-light', 'theme-dark');
    document.body.classList.add(this.value);
    localStorage.setItem('theme', this.value);
  });
});

Con muy poco código extra consigues tener tres modos diferenciados: claro, oscuro y automático según el sistema, además de persistencia entre visitas gracias a localStorage. Para probarlo bien, conviene que cambies la configuración de tema del sistema operativo y recargues la página en distintos navegadores.

Respetar las preferencias del sistema y del usuario

Una solución moderna debería intentar combinar la preferencia global del sistema (mediante prefers-color-scheme) con la decisión explícita de la persona en tu sitio (control mediante interruptor, select, etc.). Un patrón muy habitual es:

Primero, al cargar la página, mirar en localStorage si ya existe un tema elegido. Si existe, se aplica ese sin más. Si no existe, puedes preguntar al navegador mediante una media query o la API de matchMedia si el usuario prefiere dark o light, y partir de ahí.

const userPreference = localStorage.getItem('theme');

if (userPreference) {
  document.documentElement.setAttribute('tema', userPreference);
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
  document.documentElement.setAttribute('tema', 'dark');
} else {
  document.documentElement.setAttribute('tema', 'light');
}

Después, cuando la persona interactúe con tu interruptor o menú, actualizas el atributo o la clase y guardas el nuevo valor en localStorage. Así consigues un comportamiento inteligente: respetas el sistema al principio, pero en cuanto el usuario decide algo distinto, tu web recuerda esa decisión y la mantiene.

Colores, contraste y accesibilidad en modo oscuro

Montar el interruptor es la parte divertida, pero si el diseño no se lee bien, no hemos solucionado nada. En modo oscuro hay que vigilar especialmente el contraste entre fondo y texto. Un contraste demasiado bajo dificulta la lectura; uno excesivo (blanco puro sobre negro absoluto) puede cansar igual o más que un tema claro mal hecho.

Como referencia, las WCAG recomiendan un ratio de contraste de al menos 4.5:1 para texto normal y 7:1 si quieres ir sobrado. No hace falta que calcules esto a mano: hay montones de herramientas online y extensiones de navegador que te dicen si tu combinación de colores pasa el test, y a la vez revisar opciones como el modo de alto contraste.

  Cambiar El Color De Una Capa Sólida En After Effects

También es buena idea evitar el blanco puro (#ffffff) sobre negro puro (#000000) y apostar por tonos ligeramente suavizados, por ejemplo fondos casi negros como #121212 y textos en grises muy claros como #e0e0e0. Esto reduce el contraste duro y hace que el conjunto sea más agradable a la vista.

Elegir bien la paleta, tipografías y elementos de UI

En modo oscuro, los colores muy saturados cantan bastante sobre un fondo oscuro, así que conviene que uses tonos desaturados para los acentos (botones, enlaces, estados activos…). Puedes definir algo así en tus variables:

:root {
  --color-acento: #bb86fc;
}

Y luego aplicarlo a enlaces y botones:

a, button {
  color: var(--color-acento);
}

Respecto a tipografías, en temas oscuros funciona muy bien aumentar ligeramente el tamaño de la letra y el interlineado para que los bloques de texto respiren. Algo tan simple como subir un poco line-height y evitar fuentes demasiado finas ya mejora la legibilidad.

En la interfaz, asegúrate de que botones, inputs y controles interactivos son fáciles de distinguir sobre el fondo: usa bordes sutiles, sombras ligeras o variaciones de color al pasar el ratón. En dark mode, los efectos de hover y foco se vuelven especialmente importantes para indicar qué se puede pulsar, así que no los descuides.

Animaciones, movimiento y preferencias de usuario

Cuando añades transiciones suaves al cambiar entre temas, el efecto queda más pulido y menos brusco. Añadir una transición a background y color en el body suele ser suficiente:

body {
  transition: background-color 0.3s ease, color 0.3s ease;
}

No obstante, no a todo el mundo le sientan bien las animaciones, así que conviene respetar también la preferencia prefers-reduced-motion. Puedes reducir o eliminar duraciones de transiciones y animaciones cuando el usuario lo haya indicado:

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0ms !important;
    transition-duration: 0ms !important;
  }
}

De esta manera, quienes tengan sensibilidad al movimiento o mareos con animaciones no se verán obligados a tragarse efectos que les resulten molestos, y aun así el resto de usuarios disfrutará de un cambio de tema fluido.

Pruebas, compatibilidad y pequeños detalles

Una vez que tengas tu sistema de temas montado, toca la parte menos glamourosa pero imprescindible: probar en varios navegadores y dispositivos. Las DevTools de Chrome, Firefox o Safari permiten simular tanto el modo oscuro del sistema como distintas densidades de pantalla; también es útil saber cómo forzar tema oscuro en Opera. También puedes tirar de herramientas como Lighthouse para revisar accesibilidad y rendimiento.

No te olvides de revisar imágenes, iconos y logotipos. Muchas veces un logo oscuro queda perfecto en fondo blanco, pero desaparece en un escenario oscuro. En esos casos, puedes usar versiones alternativas del logo o aplicar filtros y bordes para que sigan siendo visibles en ambos temas.

Por último, es buena idea pedir feedback real: un pequeño formulario, una encuesta o incluso analizar las consultas de soporte te puede ayudar a detectar casos en los que el modo oscuro no funciona bien, combinaciones de colores problemáticas o elementos que se han quedado sin adaptar.

Teniendo claras estas piezas —variables CSS bien estructuradas, uso inteligente de prefers-color-scheme, controles de usuario con un poco de JavaScript y atención al contraste y la accesibilidad—, implementar un modo oscuro deja de ser un quebradero de cabeza para convertirse en una mejora potente que hace tu web más cómoda, moderna y alineada con las expectativas de quienes la visitan.

windows 11 actualización modo oscuro
Artículo relacionado:
Windows 11 prepara una actualización con modo oscuro completo