Maquetación Web Dinámica / Arquitectura CSS y design tokens
Theming claro/oscuro con tokens semánticos
La primera gran recompensa de tu sistema de tokens: el tema oscuro. Y no
duplicando CSS ni reescribiendo componentes, sino RE-APUNTANDO los tokens
semánticos en un solo lugar. Le sumamos prefers-color-scheme para
respetar la preferencia del sistema operativo y un toggle persistente para
que el usuario elija. Un tema oscuro de verdad, elegante y mantenible.
Los interruptores de toda la casa
Re-apuntar los semánticos
Defines los tokens semánticos para el tema claro (por defecto) y los
sobrescribes para el oscuro bajo un selector [data-theme="dark"]:
:root {
--gris-900: #0f172a; --gris-50: #f8fafc; --blanco: #fff;
/* semánticos — TEMA CLARO (default) */
--color-fondo: var(--gris-50);
--color-texto: var(--gris-900);
--color-superficie: var(--blanco);
}
[data-theme="dark"] {
/* los MISMOS semánticos, re-apuntados para OSCURO */
--color-fondo: #0b1120;
--color-texto: #f1f5f9;
--color-superficie: #151e31;
}
/* los componentes NO cambian: solo usan los semánticos */
body { background: var(--color-fondo); color: var(--color-texto); }
.tarjeta { background: var(--color-superficie); }Ni un componente se duplica. El tema entero vive en ese bloque
[data-theme="dark"]. Ahí se ve la recompensa de la capa semántica de la
lección anterior.
Pruébalo
<div class="app">
<div class="barra">
<span class="logo">◆ Forge</span>
<button id="toggle">Modo oscuro</button>
</div>
<article class="tarjeta">
<h3>Tarjeta con tokens</h3>
<p>Ni un color a mano: todo sale de tokens semánticos. El toggle solo re-apunta esos tokens.</p>
<button class="cta">Acción</button>
</article>
</div>:root {
--color-fondo: #f1f5f9; --color-texto: #0f172a; --color-suave: #64748b;
--color-superficie: #ffffff; --color-primary: #6d28d9; --color-borde: #e2e8f0;
}
[data-theme="dark"] {
--color-fondo: #0b1120; --color-texto: #f1f5f9; --color-suave: #94a3b8;
--color-superficie: #151e31; --color-primary: #a78bfa; --color-borde: #2a3752;
}
body { font-family: system-ui; margin: 0; padding: 20px; background: var(--color-fondo); color: var(--color-texto); transition: background 250ms ease, color 250ms ease; }
.barra { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.logo { font-weight: 800; color: var(--color-primary); }
#toggle { padding: 8px 14px; border: 1px solid var(--color-borde); border-radius: 8px; background: var(--color-superficie); color: var(--color-texto); font-weight: 600; cursor: pointer; }
.tarjeta { background: var(--color-superficie); border: 1px solid var(--color-borde); border-radius: 14px; padding: 20px; max-width: 340px; }
.tarjeta h3 { margin: 0 0 8px; }
.tarjeta p { margin: 0 0 14px; color: var(--color-suave); font-size: 14px; line-height: 1.6; }
.cta { padding: 10px 16px; border: 0; border-radius: 8px; background: var(--color-primary); color: white; font-weight: 600; cursor: pointer; }
@media (prefers-reduced-motion: reduce) { body { transition: none; } }const boton = document.querySelector("#toggle");
const raiz = document.documentElement;
boton.addEventListener("click", () => {
const oscuro = raiz.getAttribute("data-theme") === "dark";
raiz.setAttribute("data-theme", oscuro ? "light" : "dark");
boton.textContent = oscuro ? "Modo oscuro" : "Modo claro";
});Dale al toggle: TODO cambia —fondo, texto, superficie, acento, bordes— con
una transición suave, y ningún componente sabe que existe un tema. Fíjate
en que el violeta también cambió (a #a78bfa en oscuro): un buen acento
para fondo claro no siempre lo es para oscuro — un detalle que la capa
semántica te deja ajustar sin tocar componentes.
Respetar el sistema y persistir la elección
Dos piezas para un theming profesional:
/* AUTO: si el usuario tiene el SO en oscuro, arranca en oscuro sin JS */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-fondo: #0b1120; --color-texto: #f1f5f9; /* ...los de oscuro... */
}
}// PERSISTIR: recordar la elección manual entre visitas (localStorage, módulo 7)
const guardado = localStorage.getItem("tema");
if (guardado) document.documentElement.setAttribute("data-theme", guardado);
boton.addEventListener("click", () => {
const oscuro = document.documentElement.getAttribute("data-theme") === "dark";
const nuevo = oscuro ? "light" : "dark";
document.documentElement.setAttribute("data-theme", nuevo);
localStorage.setItem("tema", nuevo); // se recuerda al recargar
});La combinación ideal: arranca respetando el SO (prefers-color-scheme),
pero si el usuario elige manualmente, ese data-theme gana y se persiste.
(En el playground de arriba omitimos localStorage porque su iframe de
prueba lo bloquea; en una página real es el código de aquí.)
Gracias a tus tokens semánticos, ¿qué necesitas para agregar un tema oscuro completo a un producto ya construido?
Mini-reto
Dale a "El Observador" un tema oscuro completo con el sistema de tokens que
montaste: define el bloque [data-theme="dark"] re-apuntando tus
semánticos, agrega el toggle con localStorage y el @media prefers-color-scheme para el arranque automático. Pruébalo cambiando el
modo de tu sistema operativo Y con el botón. Un tema oscuro real, sin un
solo componente duplicado.
Qué sigue
Tienes los valores organizados. Falta ordenar la CASCADA misma —el otro
frente de la guerra de especificidad—. La próxima lección cubre las
metodologías: BEM para nombrar sin colisiones, @layer para controlar qué
gana en la cascada de forma explícita, y el criterio de cuándo nada de esto
hace falta.