Skip to content

Modo oscuro persistente ​

Un selector de tema Claro / Oscuro / Sistema que recuerda la elección del usuario, respeta la configuración del dispositivo por defecto y no muestra el molesto "parpadeo blanco" al cargar la página.

Demo ​

Modo oscuro persistente

Las 3 piezas ​

1. Colores con variables CSS ​

Todo el diseño usa variables. El tema oscuro sólo redefine sus valores cuando el elemento html tiene data-tema="oscuro":

css
:root {
  --fondo: #f8fafc;
  --superficie: #ffffff;
  --texto: #0f172a;
  --texto-suave: #475569;
  --borde: #e2e8f0;
  --acento: #4f46e5;
  color-scheme: light;            /* scrollbars y controles nativos claros */
}

[data-tema="oscuro"] {
  --fondo: #0b1120;
  --superficie: #111827;
  --texto: #e2e8f0;
  --texto-suave: #94a3b8;
  --borde: #1f2937;
  --acento: #818cf8;              /* acento más claro: mejor contraste sobre fondo oscuro */
  color-scheme: dark;
}

body { background: var(--fondo); color: var(--texto); transition: background .3s, color .3s; }
.tarjeta { background: var(--superficie); border: 1px solid var(--borde); }

2. Script anti-parpadeo en el head ​

Debe ir en el head, antes del CSS o justo después, sin defer, para que el tema se aplique antes de que el navegador pinte la página:

html
<script>
  (function () {
    let guardado = null;
    try { guardado = localStorage.getItem('tema'); } catch (e) {}
    const tema = guardado || (matchMedia('(prefers-color-scheme: dark)').matches ? 'oscuro' : 'claro');
    document.documentElement.dataset.tema = tema;
  })();
</script>

3. El selector ​

html
<div class="selector" role="group" aria-label="Tema">
  <button data-tema-opcion="claro">☀️ Claro</button>
  <button data-tema-opcion="oscuro">🌙 Oscuro</button>
  <button data-tema-opcion="sistema">💻 Sistema</button>
</div>
js
const CLAVE = 'tema';
const consulta = matchMedia('(prefers-color-scheme: dark)');

// localStorage puede fallar (modo privado, cookies bloqueadas): siempre con try/catch
function leer() { try { return localStorage.getItem(CLAVE); } catch { return null; } }
function guardar(v) { try { v ? localStorage.setItem(CLAVE, v) : localStorage.removeItem(CLAVE); } catch {} }

function aplicar(preferencia) {
  const tema = !preferencia || preferencia === 'sistema'
    ? (consulta.matches ? 'oscuro' : 'claro')
    : preferencia;
  document.documentElement.dataset.tema = tema;

  // Marca el botón activo (accesible)
  document.querySelectorAll('[data-tema-opcion]').forEach((b) =>
    b.setAttribute('aria-pressed', String(b.dataset.temaOpcion === (preferencia || 'sistema'))));

  // Ajusta el color de la barra del navegador en móviles
  document.querySelector('meta[name="theme-color"]')
    ?.setAttribute('content', tema === 'oscuro' ? '#0b1120' : '#ffffff');
}

document.querySelectorAll('[data-tema-opcion]').forEach((btn) => {
  btn.addEventListener('click', () => {
    const opcion = btn.dataset.temaOpcion;
    guardar(opcion === 'sistema' ? null : opcion); // "sistema" = no guardar nada
    aplicar(opcion);
  });
});

// Si sigue al sistema, reacciona en vivo cuando el usuario cambia su SO a oscuro
consulta.addEventListener('change', () => { if (!leer()) aplicar('sistema'); });

aplicar(leer() || 'sistema');

Variante: un solo botón que alterna ​

html
<button id="btn-tema" aria-label="Cambiar tema">🌙</button>
js
const btn = document.getElementById('btn-tema');
const pintarIcono = () => (btn.textContent = document.documentElement.dataset.tema === 'oscuro' ? '☀️' : '🌙');

btn.addEventListener('click', () => {
  const nuevo = document.documentElement.dataset.tema === 'oscuro' ? 'claro' : 'oscuro';
  document.documentElement.dataset.tema = nuevo;
  try { localStorage.setItem('tema', nuevo); } catch {}
  pintarIcono();
});
pintarIcono();

Imágenes y logos en modo oscuro ​

css
/* Baja un poco el brillo de las fotos */
[data-tema="oscuro"] img:not(.logo) { filter: brightness(.88); }

/* Logo distinto para cada tema */
.logo-oscuro { display: none; }
[data-tema="oscuro"] .logo-claro { display: none; }
[data-tema="oscuro"] .logo-oscuro { display: inline; }

O con la etiqueta picture, que respeta la preferencia del sistema:

html
<picture>
  <source srcset="logo-blanco.svg" media="(prefers-color-scheme: dark)">
  <img src="logo.svg" alt="Café Aurora" class="logo">
</picture>

Contraste

En modo oscuro no uses negro puro (#000) ni blanco puro para el texto: cansa la vista. Fondos como #0b1120 o #111827 y texto #e2e8f0 se leen mucho mejor. Verifica el contraste con la herramienta de DevTools (mínimo 4.5:1).

Error común

Cargar el script del tema al final del body. La página se pinta primero en claro y luego "salta" a oscuro, un destello muy molesto. La parte que lee la preferencia debe ir en el head.

Academia Web — aprende, copia y construye.