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.