tutoriales.com

Desarrollo de Temas Oscuros y Claros en Angular: ¡Modos Dinámicos con Tailwind CSS!

Este tutorial te guiará paso a paso en la creación de un sistema de temas dinámico (claro/oscuro) en tus aplicaciones Angular. Utilizaremos Tailwind CSS para una estilización eficiente y una lógica de TypeScript robusta para la gestión del estado del tema. Mejora la experiencia de usuario y la accesibilidad de tus proyectos.

Intermedio20 min de lectura6 views
Reportar error

🚀 Introducción: Temas Dinámicos en Angular

En la era digital actual, la flexibilidad y la personalización son claves para una excelente experiencia de usuario. Una de las características más demandadas y valoradas es la posibilidad de alternar entre un tema claro y un tema oscuro. Esto no solo mejora la accesibilidad, especialmente para usuarios con sensibilidad a la luz o aquellos que prefieren interfaces de bajo contraste, sino que también permite a los usuarios adaptar la aplicación a sus preferencias personales o al entorno lumínico. En este tutorial, aprenderemos a implementar un sistema robusto de temas dinámicos en una aplicación Angular, aprovechando la potencia de Tailwind CSS para una estilización rápida y limpia.

¿Por qué implementar temas claro/oscuro?

  • Accesibilidad: Reduce la fatiga visual, especialmente en entornos con poca luz o para usuarios con ciertas condiciones visuales.
  • Preferencias del usuario: Ofrece control al usuario sobre la apariencia de la aplicación.
  • Estética moderna: Una característica esperada en las aplicaciones modernas, que demuestra atención al detalle.
  • Eficiencia energética: Los temas oscuros pueden ahorrar batería en dispositivos con pantallas OLED.

🛠️ Requisitos Previos

Antes de sumergirnos en el código, asegúrate de tener lo siguiente instalado en tu sistema:

  • Node.js y npm: Asegúrate de tener una versión LTS (Long Term Support) instalada.
  • Angular CLI: Si no lo tienes, puedes instalarlo globalmente con npm install -g @angular/cli.
  • Conocimientos básicos de Angular: Componentes, servicios, enlaces de datos.
  • Conocimientos básicos de Tailwind CSS: Clases utilitarias.
🔥 Importante: Este tutorial asume que ya tienes una aplicación Angular creada o que sabes cómo crear una nueva. Si necesitas crear una, usa `ng new my-theme-app`.

⚙️ Configuración Inicial del Proyecto

Primero, crearemos una nueva aplicación Angular (si no la tienes ya) e integraremos Tailwind CSS.

Paso 1: Crear una Nueva Aplicación Angular (Si es necesario)

Si ya tienes un proyecto Angular, puedes omitir este paso.

ng new angular-dark-mode --style=scss --routing=false
cd angular-dark-mode

Paso 2: Instalar y Configurar Tailwind CSS

Para integrar Tailwind CSS en tu proyecto Angular, sigue estos comandos:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Esto creará los archivos tailwind.config.js y postcss.config.js en la raíz de tu proyecto.

Ahora, edita tailwind.config.js para configurar las rutas de tus archivos y habilitar el modo oscuro basado en clases (class-based dark mode):

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: 'class', // Habilitar el modo oscuro basado en clases
  content: [
    "./src/**/*.{html,ts}"
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
📌 Nota: Al establecer `darkMode: 'class'`, Tailwind CSS añadirá clases como `dark:bg-gray-800` a tu CSS. Cuando la clase `dark` esté presente en un elemento ancestral (típicamente `` o ``), estas clases de modo oscuro se aplicarán.

Finalmente, incluye las directivas de Tailwind en tu archivo principal de estilos. Abre src/styles.scss (o styles.css si no usaste Sass) y añade lo siguiente:

/* src/styles.scss */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Estilos globales por defecto (claro) */
body {
  @apply bg-gray-100 text-gray-900;
  transition: background-color 0.3s ease, color 0.3s ease; /* Transición suave */
}

/* Estilos para el modo oscuro */
body.dark {
  @apply bg-gray-900 text-gray-100;
}
💡 Consejo: Añadir transiciones a las propiedades `background-color` y `color` en `body` hace que el cambio de tema sea visualmente más agradable y suave.

💡 Creación del Servicio de Tema

Para gestionar el estado del tema (claro u oscuro) y permitir que los componentes interactúen con él, crearemos un ThemeService.

Paso 1: Generar el Servicio

ng generate service core/theme

Esto creará src/app/core/theme.service.ts.

Paso 2: Implementar la Lógica del Servicio

Modifica src/app/core/theme.service.ts para que maneje el estado del tema, lo persista en localStorage y actualice la clase dark en el elemento <body>.

// src/app/core/theme.service.ts
import { Injectable, Renderer2, RendererFactory2 } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

type Theme = 'light' | 'dark';

@Injectable({
  providedIn: 'root'
})
export class ThemeService {
  private readonly THEME_KEY = 'theme';
  private _currentTheme = new BehaviorSubject<Theme>('light');
  public currentTheme$: Observable<Theme> = this._currentTheme.asObservable();

  private renderer: Renderer2;

  constructor(rendererFactory: RendererFactory2) {
    this.renderer = rendererFactory.createRenderer(null, null);
    this.loadTheme();
  }

  /**
   * Carga el tema guardado en localStorage o detecta la preferencia del sistema
   * y lo aplica al inicio de la aplicación.
   */
  private loadTheme(): void {
    const savedTheme = localStorage.getItem(this.THEME_KEY) as Theme;
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

    let initialTheme: Theme = 'light';
    if (savedTheme) {
      initialTheme = savedTheme;
    } else if (prefersDark) {
      initialTheme = 'dark';
    }

    this.applyTheme(initialTheme);
  }

  /**
   * Aplica un tema específico a la aplicación.
   * @param theme El tema a aplicar ('light' o 'dark').
   */
  private applyTheme(theme: Theme): void {
    this._currentTheme.next(theme);
    localStorage.setItem(this.THEME_KEY, theme);

    if (theme === 'dark') {
      this.renderer.addClass(document.body, 'dark');
    } else {
      this.renderer.removeClass(document.body, 'dark');
    }
  }

  /**
   * Alterna entre el tema claro y oscuro.
   */
  toggleTheme(): void {
    const newTheme = this._currentTheme.getValue() === 'light' ? 'dark' : 'light';
    this.applyTheme(newTheme);
  }

  /**
   * Devuelve el tema actual.
   * @returns El tema actual ('light' o 'dark').
   */
  getCurrentTheme(): Theme {
    return this._currentTheme.getValue();
  }
}

Explicación del Servicio:

  • _currentTheme: Un BehaviorSubject para mantener el estado del tema actual y notificar a los suscriptores sobre cambios.
  • THEME_KEY: Una constante para la clave de localStorage.
  • renderer: Se utiliza Renderer2 para manipular el DOM de forma segura y agnóstica a la plataforma, añadiendo o quitando la clase dark del <body>.
  • loadTheme(): Al inicializarse el servicio, comprueba si hay un tema guardado en localStorage. Si no, detecta la preferencia de tema del sistema operativo ((prefers-color-scheme: dark)). Finalmente, aplica ese tema inicial.
  • applyTheme(): Actualiza el BehaviorSubject, guarda el tema en localStorage y añade/elimina la clase dark del <body>.
  • toggleTheme(): Método público para cambiar entre 'light' y 'dark'.

🎨 Creación del Componente de Conmutación de Tema

Necesitaremos un componente para permitir al usuario cambiar el tema. Este componente será un simple interruptor (toggle button).

Paso 1: Generar el Componente

ng generate component shared/theme-switcher

Esto creará la carpeta src/app/shared/theme-switcher con sus archivos.

Paso 2: Implementar la Lógica del Componente

Modifica src/app/shared/theme-switcher/theme-switcher.component.ts para inyectar ThemeService y obtener el tema actual.

// src/app/shared/theme-switcher/theme-switcher.component.ts
import { Component, OnInit } from '@angular/core';
import { ThemeService } from '../../core/theme.service';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-theme-switcher',
  templateUrl: './theme-switcher.component.html',
  styleUrls: ['./theme-switcher.component.scss']
})
export class ThemeSwitcherComponent implements OnInit {
  currentTheme$: Observable<'light' | 'dark'>;

  constructor(private themeService: ThemeService) {
    this.currentTheme$ = this.themeService.currentTheme$;
  }

  ngOnInit(): void {}

  toggleTheme(): void {
    this.themeService.toggleTheme();
  }
}

Paso 3: Diseñar la Interfaz del Conmutador

Modifica src/app/shared/theme-switcher/theme-switcher.component.html para incluir un botón que llame a toggleTheme() y muestre el tema actual.

<!-- src/app/shared/theme-switcher/theme-switcher.component.html -->
<button
  (click)="toggleTheme()"
  class="p-2 rounded-full shadow-lg transition-colors duration-300 ease-in-out
         bg-white text-gray-800 dark:bg-gray-700 dark:text-white
         hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
  <ng-container *ngIf="(currentTheme$ | async) === 'light'; else darkThemeIcon">
    <!-- Icono para tema claro (sol) -->
    
```
💡 Consejo: Hemos usado clases de Tailwind como `dark:bg-gray-700` y `dark:text-white`. Estas clases solo se aplicarán cuando el `body` tenga la clase `dark`, lo que será controlado por nuestro `ThemeService`.

🌐 Integración en la Aplicación Principal

Ahora que tenemos el servicio y el componente de conmutación, debemos integrarlos en nuestra aplicación principal.

Paso 1: Importar ThemeSwitcherComponent en AppModule

Para poder usar ThemeSwitcherComponent en cualquier parte de tu aplicación, necesitas declararlo en el AppModule o en un módulo compartido que se importe en el AppModule.

// src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { ThemeSwitcherComponent } from './shared/theme-switcher/theme-switcher.component';

@NgModule({
  declarations: [
    AppComponent,
    ThemeSwitcherComponent // Declarar el componente aquí
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Paso 2: Usar ThemeSwitcherComponent en AppComponent

Modifica src/app/app.component.html para incluir el conmutador de tema y algunos elementos con estilo para probarlo.

<!-- src/app/app.component.html -->
<div class="min-h-screen flex flex-col items-center justify-center p-4">
  <header class="w-full max-w-2xl flex justify-end mb-8">
    <app-theme-switcher></app-theme-switcher>
  </header>

  <main class="w-full max-w-2xl p-8 rounded-lg shadow-xl 
               bg-white dark:bg-gray-800 
               text-gray-900 dark:text-gray-100 
               transition-colors duration-300 ease-in-out">
    <h1 class="text-4xl font-bold mb-4 text-center 
               text-blue-600 dark:text-blue-400">
      ¡Bienvenido a Tu Aplicación con Temas Dinámicos!
    </h1>
    <p class="text-lg leading-relaxed mb-6 text-center 
              text-gray-700 dark:text-gray-300">
      Este es un ejemplo de cómo puedes implementar un sistema de tema oscuro/claro
      utilizando Angular y Tailwind CSS. Prueba a cambiar el tema con el botón de arriba.
    </p>

    <div class="mt-8 grid grid-cols-1 md:grid-cols-2 gap-6">
      <div class="p-6 rounded-lg shadow-md 
                  bg-gray-50 dark:bg-gray-700 
                  text-gray-800 dark:text-gray-200">
        <h3 class="font-semibold text-xl mb-2 
                   text-purple-600 dark:text-purple-400">Modo Claro</h3>
        <p>Cuando el tema es claro, verás este estilo.</p>
        <p class="text-sm mt-2 text-gray-500 dark:text-gray-400">Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
      </div>
      <div class="p-6 rounded-lg shadow-md 
                  bg-gray-50 dark:bg-gray-700 
                  text-gray-800 dark:text-gray-200">
        <h3 class="font-semibold text-xl mb-2 
                   text-green-600 dark:text-green-400">Modo Oscuro</h3>
        <p>Cuando el tema es oscuro, los colores cambiarán.</p>
        <p class="text-sm mt-2 text-gray-500 dark:text-gray-400">Excepturi, commodi ducimus voluptate, neque, quos corrupti.</p>
      </div>
    </div>

    <footer class="mt-10 pt-6 border-t border-gray-200 dark:border-gray-700 text-center 
                   text-gray-600 dark:text-gray-400">
      <p>&copy; 2023 Tutorial de Temas Dinámicos.</p>
    </footer>
  </main>
</div>

Explicación del AppComponent HTML:

  • El div principal tiene min-h-screen para asegurar que el fondo se extienda por toda la altura de la pantalla.
  • Hemos añadido app-theme-switcher en el header para que el botón esté visible en la esquina superior derecha.
  • Observa cómo se usan las clases dark: de Tailwind en varios elementos (main, h1, p, div de tarjetas) para definir estilos específicos cuando el body tiene la clase dark.
  • Las clases transition-colors duration-300 ease-in-out proporcionan una animación suave al cambiar los colores de fondo y texto, lo que mejora la experiencia visual.

Paso 3: Asegurarse de que el servicio se inicialice correctamente

Nuestro ThemeService se inicializa al ser inyectado por primera vez (ya que está providedIn: 'root'). Para asegurarnos de que loadTheme() se ejecute al inicio de la aplicación y el tema correcto se aplique antes de que Angular renderice la vista, podemos inyectar ThemeService en el constructor de AppComponent.

// src/app/app.component.ts
import { Component } from '@angular/core';
import { ThemeService } from './core/theme.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'angular-dark-mode';

  constructor(private themeService: ThemeService) {
    // El servicio se inicializará y el tema se cargará aquí
  }
}
⚠️ Advertencia: Si no inyectas el `ThemeService` en un componente raíz como `AppComponent` o en un servicio que se inicialice al arranque, la lógica de `loadTheme()` podría no ejecutarse a tiempo, resultando en un parpadeo o una carga incorrecta del tema inicial.

✨ Estructura del Proyecto Final

Tu estructura de archivos debería verse algo así:

angular-dark-mode/
├── src/
│   ├── app/
│   │   ├── core/
│   │   │   └── theme.service.ts
│   │   ├── shared/
│   │   │   └── theme-switcher/
│   │   │       ├── theme-switcher.component.html
│   │   │       ├── theme-switcher.component.scss
│   │   │       └── theme-switcher.component.ts
│   │   ├── app.component.html
│   │   ├── app.component.scss
│   │   ├── app.component.ts
│   │   └── app.module.ts
│   └── styles.scss
├── tailwind.config.js
└── postcss.config.js

✅ ¡Pruébalo!

Ahora, inicia tu aplicación Angular para ver los temas en acción.

ng serve -o

Abre tu navegador y navega a http://localhost:4200. Deberías ver la aplicación cargada con el tema por defecto (claro o el que detecte tu sistema operativo). Haz clic en el botón del conmutador para alternar entre el modo claro y oscuro. Verás cómo los colores de fondo, texto y de los componentes cambian dinámicamente.

1. Usuario Hace clic en el interruptor de tema 2. ThemeSwitcherComponent themeService.toggleTheme() 3. ThemeService Actualiza variable de estado _currentTheme Guarda preferencia en localStorage Usa Renderer2 para manipular el DOM: document.body.classList.toggle('dark') 4. Tailwind CSS & UI Detecta clase 'dark' en el <body> Aplica selectores 'dark:' a todos los componentes

📝 Consideraciones Adicionales y Mejoras

1. Personalización Avanzada de Temas

Si necesitas más que solo dos modos o quieres temas más complejos, puedes extender el ThemeService para que reciba un objeto de configuración de tema. Podrías definir variables CSS o clases específicas para cada tema y aplicarlas dinámicamente.

Ejemplo: Temas Múltiples

En tailwind.config.js, puedes definir colores personalizados:

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  content: [
    "./src/**/*.{html,ts}"
  ],
  theme: {
    extend: {
      colors: {
        'custom-blue': {
          light: '#3B82F6',
          dark: '#60A5FA',
        },
        'custom-red': {
          light: '#EF4444',
          dark: '#F87171',
        },
      },
    },
  },
  plugins: [],
}

Luego, puedes usar estas variables en tu HTML:

<h1 class="text-custom-blue-light dark:text-custom-blue-dark">Título</h1>

Para temas más complejos, podrías tener un servicio que cargue hojas de estilo dinámicamente o que inyecte variables CSS en el document.documentElement.

2. Iconos Dinámicos

En nuestro ejemplo, usamos ngIf para cambiar los iconos. Para aplicaciones más grandes, considera usar una librería de iconos como Font Awesome o un enfoque más modular para la gestión de iconos SVG.

3. Rendimiento y SEO (Server-Side Rendering)

Para aplicaciones Angular que utilizan Server-Side Rendering (SSR) con Angular Universal, es importante que el tema inicial se renderice correctamente en el servidor. Nuestro ThemeService ya maneja la detección de preferencias del sistema, lo que ayuda a que el servidor pueda renderizar la versión inicial con el tema preferido del usuario, mejorando el LCP (Largest Contentful Paint) y la experiencia de usuario.

4. Accesibilidad (ARIA Attributes)

Para mejorar la accesibilidad del conmutador de tema, considera añadir atributos ARIA. Por ejemplo:

<button
  (click)="toggleTheme()"
  [attr.aria-label]="(currentTheme$ | async) === 'light' ? 'Cambiar a modo oscuro' : 'Cambiar a modo claro'"
  [attr.aria-pressed]="(currentTheme$ | async) === 'dark'"
  role="switch"
>
  <!-- ... iconos ... -->
</button>

🏁 Conclusión

Hemos cubierto todos los pasos necesarios para implementar un sistema de temas dinámicos (claro/oscuro) en tu aplicación Angular utilizando Tailwind CSS. Desde la configuración inicial de Tailwind, la creación de un ThemeService robusto, hasta la integración del componente de conmutación en tu aplicación principal, este tutorial te proporciona una base sólida para ofrecer una experiencia de usuario más personalizada y accesible.

La flexibilidad de Tailwind CSS combinada con la reactividad de Angular y la persistencia del estado en localStorage crea una solución potente y fácil de mantener. ¡Ahora puedes llevar tus aplicaciones Angular al siguiente nivel de usabilidad y diseño!

💡 Consejo Final: Anima a tus usuarios a probar ambos modos y a dar retroalimentación sobre cuál prefieren. ¡La elección es suya!

❓ Preguntas Frecuentes (FAQ)

¿Puedo usar CSS puro en lugar de Tailwind CSS? Sí, claro. La lógica del `ThemeService` es independiente de la librería CSS que uses. En lugar de clases `dark:`, tendrías que definir reglas CSS con un selector `.dark` en tu `styles.scss` o en los CSS de tus componentes.

Por ejemplo:

/* En un componente o global */
.mi-tarjeta {
  background-color: white;
  color: black;
}

body.dark .mi-tarjeta {
  background-color: #333;
  color: white;
}
¿Qué pasa si el usuario desactiva JavaScript? Si JavaScript está deshabilitado, el `ThemeService` no podrá manipular la clase `dark` en el ``, y el conmutador de tema no funcionará. La aplicación se mostrará con el tema por defecto que esté definido en tu `styles.scss` (el `body` sin la clase `dark`).
¿Cómo puedo testear el tema oscuro en desarrollo? Además de usar el conmutador, puedes cambiar la preferencia de tema de tu sistema operativo o en las herramientas de desarrollo del navegador. En Chrome DevTools, por ejemplo, puedes ir a la pestaña 'Rendering' y emular `prefers-color-scheme: dark`.

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!