Tema
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
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.