Por qué el dark mode ya no es opcional
El dark mode paso de ser un "nice to have" a una expectativa de los usuarios. Estudios muestran que más del 80% de usuarios activan el modo oscuro cuando esta disponible. Ademas de la preferencia estetica, reduce la fatiga visual en ambientes con poca luz y ahorra bateria en pantallas OLED.
En esta guia vamos a implementar un sistema de temas robusto usando CSS custom properties y Angular signals que soporta tres modos: claro, oscuro y sistema.
La estrategia: CSS Custom Properties
El enfoque más limpio para dark mode es usar CSS custom properties (variables CSS) como design tokens. En lugar de escribir estilos diferentes para cada tema, defines tus colores como variables y cambias sus valores segun el tema activo.
Definiendo los tokens
El primer paso es definir tus design tokens en :root para el tema claro (por defecto) y en [data-theme="dark"] para el tema oscuro. Mira el primer bloque de código para ver la estructura.
Por qué data-theme en lugar de clase
Usamos el atributo data-theme en el elemento <html> en lugar de una clase CSS por varias razones:
- Separacion semántica: los atributos data son para datos, las clases para estilos
- Evita colisiones con clases utilitarias de Tailwind o Bootstrap
- Es más fácil seleccionar con
[data-theme="dark"]que con.dark - Soporta más de dos valores (podrias tener
data-theme="sepia"en el futuro)
Usando los tokens en tus componentes
Una vez definidos los tokens, usarlos en cualquier componente es natural:
.card {
background: var(--color-bg-elevated);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-sm);
border-radius: 12px;
padding: 1.5rem;
}
.card-subtitle {
color: var(--color-text-secondary);
}No necesitas ningun @if ni media query. Los colores se actualizan automaticamente cuando cambia el atributo data-theme.
El ThemeService en Angular
El servicio de tema es el cerebro del sistema. Usa Angular signals para un manejo de estado reactivo y limpio. Revisa el segundo bloque de código para ver la implementación completa.
Puntos clave del servicio
Tres modos, no dos: Ademas de "light" y "dark", soportamos "system" que respeta la preferencia del sistema operativo. Esto es lo correcto en términos de UX.
Persistencia: Guardamos la preferencia en localStorage para que sobreviva entre sesiones.
Reactive system changes: Escuchamos cambios en prefers-color-scheme del sistema para actualizar en tiempo real si el usuario tiene seleccionado el modo "system".
Effect para sincronizar: Usamos effect() para que cada vez que cambie el signal theme, se actualice el DOM y se persista la preferencia automaticamente.
El componente de toggle
El componente de toggle es simple pero accesible. Revisa el tercer bloque de código. Aspectos importantes:
- aria-label dinámico: Informa al screen reader cual es el tema actual
- Iconos con aria-hidden: Los SVG son decorativos, no informativos
- Tres estados: El boton cicla entre light, dark y system
- Type button: Previene submit accidental si esta dentro de un form
Transiciones suaves entre temas
Para que el cambio de tema no sea abrupto, agrega transiciones a las propiedades que usan tus tokens:
:root {
--transition-theme: background-color 200ms ease, color 200ms ease,
border-color 200ms ease, box-shadow 200ms ease;
}
body {
transition: var(--transition-theme);
}
.card,
.navbar,
.sidebar,
.footer {
transition: var(--transition-theme);
}Cuidado con transition: all
No uses transition: all para el cambio de tema. Es tentador, pero causa problemas:
- Anima propiedades que no deberias (como
width,height) - Impacta el rendimiento (el navegador tiene que verificar todas las propiedades)
- Puede causar parpadeos extrannos en elementos con animaciones propias
Se explícito con las propiedades que quieres animar.
Previniendo el flash of unstyled theme
Si el tema se aplica via JavaScript, hay un momento breve donde el tema por defecto (claro) se muestra antes de que el script ejecute. Esto se llama FOUT (Flash of Unstyled Theme) y es molesto para usuarios con dark mode.
Solucion: script bloqueante en el head
Agrega un script inline en el <head> de tu index.html:
<script>
(function() {
const theme = localStorage.getItem('preferred-theme');
const resolved = theme === 'dark' ? 'dark'
: theme === 'light' ? 'light'
: window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', resolved);
})();
</script>Este script se ejecuta antes de que se pinte la página, eliminando el flash. Es pequeño y bloqueante a propósito.
Imagenes adaptativas al tema
Algunos assets necesitan versiones diferentes para cada tema (logos, ilustraciones):
<picture>
<source
srcset="/images/logo-dark.svg"
media="(prefers-color-scheme: dark)" />
<img
src="/images/logo-light.svg"
alt="Logo" />
</picture>Para imagenes controladas por tu atributo data-theme, usa CSS:
.logo-img {
content: url('/images/logo-light.svg');
}
[data-theme="dark"] .logo-img {
content: url('/images/logo-dark.svg');
}Accesibilidad en dark mode
El dark mode tiene desafios de accesibilidad específicos:
Contraste
- No uses blanco puro (#ffffff) sobre negro puro (#000000). Es demasiado contraste y causa fatiga
- Usa blanco suave (#f0f0f5) sobre gris muy oscuro (#0a0a0f)
- Verifica el contraste de ambos temas con herramientas como el contrast checker de WebAIM
Colores de acento
Tu color de acento puede necesitar ajustes entre temas. Un naranja que se ve bien sobre fondo blanco puede no tener suficiente contraste sobre fondo oscuro. Ajusta la luminosidad en tus tokens dark.
Focus indicators
Los indicadores de focus deben ser visibles en ambos temas. Un outline azul funciona en light mode pero puede perderse en dark mode. Define focus styles por tema:
:root {
--color-focus: #0066ff;
}
[data-theme="dark"] {
--color-focus: #66aaff;
}
:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}Testing del dark mode
Verifica tu implementación con esta checklist:
- El tema se persiste al recargar la página
- No hay flash del tema incorrecto al cargar
- El modo "system" responde a cambios del OS en tiempo real
- Todos los textos pasan contraste WCAG AA en ambos temas
- Los focus indicators son visibles en ambos temas
- Las imagenes y logos se ven correctamente en ambos temas
- Las transiciones son suaves y no causan parpadeos
Conclusion
Un dark mode bien implementado mejora la experiencia del usuario, demuestra atención al detalle y es esperado en cualquier aplicación web moderna. Con CSS custom properties como foundation y Angular signals para el manejo de estado, tienes un sistema robusto, mantenible y accesible.
La clave esta en tratarlo como un sistema de design tokens, no como un hack de colores invertidos. Esto te prepara para expandir a más temas en el futuro si es necesario.



Comentarios (0)
Inicia sesión para comentar