Dark Mode
Dark mode that feels native — light, dark, or follow the system.
How it Works
A quick look under the hood.
Dark mode is implemented using a .dark class on the <html> element. When this class is present, Tailwind's dark mode variants become active, applying the dark theme variables.
No Flash of Unstyled Content
The theme is set before first paint by reading localStorage in a head script.
Tailwind Configuration
Enable dark mode variants.
Add the dark mode variant to your styles.scss:
@import "tailwindcss";
/* Custom dark mode variant - activates styles inside .dark class */
@custom-variant dark (&:is(.dark *));Theme Service
Manage theme state with signals.
A small service that applies the theme and stays in sync with system preferences:
import { computed, effect, Injectable, signal } from '@angular/core';
export type Theme = 'light' | 'dark' | 'system';
@Injectable({ providedIn: 'root' })
export class ThemeService {
readonly theme = signal<Theme>(this.getStoredTheme());
readonly isDark = computed(() => {
const theme = this.theme();
if (theme === 'system') {
return this.systemPrefersDark();
}
return theme === 'dark';
});
private readonly systemPrefersDark = signal(
this.getSystemPreference()
);
constructor() {
// Apply theme class reactively
effect(() => {
document.documentElement.classList.toggle('dark', this.isDark());
});
// Listen for system preference changes
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
this.systemPrefersDark.set(e.matches);
});
}
setTheme(theme: Theme): void {
this.theme.set(theme);
localStorage.setItem('theme', theme);
}
private getStoredTheme(): Theme {
return (localStorage.getItem('theme') as Theme) || 'system';
}
private getSystemPreference(): boolean {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
}Theme Toggle Component
Let users choose a theme.
import { Component, inject } from '@angular/core';
import { ThemeService, Theme } from './theme.service';
import { Button } from '@/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/ui/dropdown-menu';
import { Sun, Moon } from 'lucide-angular';
@Component({
selector: 'ThemeToggle',
imports: [
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
],
template: `
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant="ghost" size="icon">
@if (themeService.isDark()) {
<lucide-icon [img]="Moon" class="h-5 w-5" />
} @else {
<lucide-icon [img]="Sun" class="h-5 w-5" />
}
<span class="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem (click)="setTheme('light')">
Light
</DropdownMenuItem>
<DropdownMenuItem (click)="setTheme('dark')">
Dark
</DropdownMenuItem>
<DropdownMenuItem (click)="setTheme('system')">
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
`,
})
export class ThemeToggle {
protected readonly themeService = inject(ThemeService);
protected setTheme(theme: Theme): void {
this.themeService.setTheme(theme);
}
}Usage
Drop it into your app.
<!-- In your header component template -->
<header class="flex items-center justify-between border-b px-4 py-3">
<a routerLink="/" class="font-semibold">
My App
</a>
<nav class="flex items-center gap-4">
<a routerLink="/docs">Docs</a>
<a routerLink="/components">Components</a>
</nav>
<ThemeToggle />
</header>Live Preview
Current theme: system
System Preference Detection
Automatically follows your OS setting.
When the theme is set to "system", the service listens to the prefers-color-scheme media query and updates when the user changes their system preference.
Light Mode
Used when the system is set to light
Dark Mode
Used when the system is set to dark