Bootstrap 5.3's native dark mode support makes implementing dark theme in Angular clean and simple. No custom CSS switching, no toggling classes on hundreds of elements — just set one attribute on the html element.

Here's a complete service-based implementation using Angular 21 signals.

The Theme Service

// theme.service.ts
import { Injectable, signal, effect } from '@angular/core'

type Theme = 'light' | 'dark'

@Injectable({ providedIn: 'root' })
export class ThemeService {
  // Signal holds the current theme
  theme = signal<Theme>(this.getInitialTheme())

  constructor() {
    // Effect runs whenever theme signal changes
    effect(() => {
      this.applyTheme(this.theme())
    })
  }

  toggle() {
    this.theme.update(current => current === 'light' ? 'dark' : 'light')
  }

  setTheme(theme: Theme) {
    this.theme.set(theme)
  }

  isDark() {
    return this.theme() === 'dark'
  }

  private getInitialTheme(): Theme {
    // 1. Check localStorage
    const saved = localStorage.getItem('theme') as Theme | null
    if (saved === 'light' || saved === 'dark') return saved

    // 2. Check system preference
    if (window.matchMedia('(prefers-color-scheme: dark)').matches) return 'dark'

    // 3. Default to light
    return 'light'
  }

  private applyTheme(theme: Theme) {
    document.documentElement.setAttribute('data-bs-theme', theme)
    localStorage.setItem('theme', theme)
  }
}

The effect() function runs automatically whenever theme signal changes. No subscriptions to manage, no ngOnDestroy cleanup needed.

The Toggle Button Component

// theme-toggle.component.ts
import { Component, inject } from '@angular/core'
import { CommonModule } from '@angular/common'
import { ThemeService } from './theme.service'

@Component({
  selector: 'app-theme-toggle',
  standalone: true,
  imports: [CommonModule],
  template: `
    <button
      class="btn btn-sm btn-outline-secondary d-flex align-items-center gap-2"
      (click)="themeService.toggle()"
      [attr.aria-label]="themeService.isDark() ? 'Switch to light mode' : 'Switch to dark mode'">
      {{ themeService.isDark() ? '☀️' : '🌙' }}
      <span class="d-none d-sm-inline">
        {{ themeService.isDark() ? 'Light' : 'Dark' }}
      </span>
    </button>
  `
})
export class ThemeToggleComponent {
  themeService = inject(ThemeService)
}

Add to your navbar:

<app-theme-toggle />

That's all. The service handles everything — applying the theme, persisting it, reading system preference on first load.

Using Theme State in Components

Read the current theme anywhere using the signal:

// any.component.ts
import { Component, inject, computed } from '@angular/core'
import { ThemeService } from './theme.service'

@Component({
  standalone: true,
  template: `
    <div [class]="containerClass()">
      Content that changes with theme
    </div>
  `
})
export class SomeComponent {
  private themeService = inject(ThemeService)

  containerClass = computed(() =>
    this.themeService.isDark()
      ? 'bg-dark text-white p-4 rounded'
      : 'bg-light text-dark p-4 rounded'
  )
}

computed() creates a derived signal that automatically updates when theme changes.

System Preference Listener

To respond to system dark mode changes in real time (user changes OS settings while the app is open):

// Add to ThemeService constructor
constructor() {
  effect(() => {
    this.applyTheme(this.theme())
  })

  // Listen for system preference changes
  // Only update if user hasn't manually set a preference
  window.matchMedia('(prefers-color-scheme: dark)')
    .addEventListener('change', (e) => {
      const hasManualPreference = localStorage.getItem('theme')
      if (!hasManualPreference) {
        this.theme.set(e.matches ? 'dark' : 'light')
      }
    })
}

Custom Dark Mode Colors

Bootstrap's dark mode uses CSS variables you can override in your styles.scss:

[data-bs-theme="dark"] {
  --bs-body-bg: #0a0f1e;          // Deep dark background
  --bs-body-color: #e2e8f0;       // Light text
  --bs-card-bg: #111827;          // Dark card background
  --bs-border-color: #1e293b;     // Subtle borders
  --bs-secondary-bg: #0f172a;     // Secondary background
  --bs-tertiary-bg: #0d0d0d;      // Darkest background

  // Custom brand colors stay consistent
  --bs-primary: #fd4766;
  --bs-primary-rgb: 253, 71, 102;
}

Applying Dark Theme to Specific Components Only

If you want a dark sidebar on a light app:

// sidebar.component.ts
import { Component, ElementRef, OnInit, inject } from '@angular/core'
import { ThemeService } from './theme.service'

@Component({
  selector: 'app-sidebar',
  standalone: true,
  // Note: NO data-bs-theme here — we set it programmatically
  template: `<nav class="sidebar"><!-- content --></nav>`,
  host: { 'attr.data-bs-theme': 'dark' }  // Always dark sidebar
})
export class SidebarComponent {}

Or for a sidebar that's always dark regardless of global theme, just set it in the template:

<aside data-bs-theme="dark" style="background:#0d0d0d;">
  <!-- This sidebar is always dark -->
</aside>

Clean, simple, and Bootstrap handles all the component styling inside automatically.

Frequently Asked Questions

Set data-bs-theme='dark' on the document.documentElement (the html element). Bootstrap reads this attribute and applies dark colors to all its components via CSS variables. Angular manages when and how to set this attribute.
A service is cleaner — it centralizes the logic, persists to localStorage, detects system preference and can be injected anywhere. A directive adds unnecessary complexity for something that operates on the document root.
Create a signal for the current theme in your service: theme = signal<'light'|'dark'>('light'). Use effect() to react to changes and update document.documentElement. Components can read theme() directly and Angular's reactivity handles updates.
Instead of setting data-bs-theme on documentElement, set it on a specific element using a @ViewChild reference or the Renderer2 service: this.renderer.setAttribute(this.sidebarEl.nativeElement, 'data-bs-theme', 'dark').
Yes if you use localStorage. In your theme service, save to localStorage when the theme changes and read from it on initialization. Angular services are re-created on each page load so you need localStorage for persistence.

Related

Need a Full Angular + Bootstrap Admin Dashboard?

Marvel Angular Dashboard — Angular 21 + Bootstrap 5 with 50+ components and dark mode.

Browse Templates →

Use code FIRST30 for 30% off.