Implementing Dark Mode in Angular Applications: A Comprehensive Guide
Dark mode has become a popular feature in modern web applications, offering users a visually comfortable experience in low-light environments and enhancing accessibility. In Angular, implementing dark mode involves creating a theming system that toggles between light and dark styles, often using CSS custom properties, Angular Material theming, or a combination of both. This guide provides an in-depth exploration of implementing dark mode in Angular applications, covering setup, implementation, and advanced techniques to ensure a seamless and user-friendly experience. We’ll discuss why dark mode is valuable, how to configure your Angular project, and practical steps to create a robust theming system, empowering you to deliver adaptable, modern Angular applications.
Why Implement Dark Mode in Angular?
Dark mode, which uses light text on a dark background, offers several benefits that enhance user experience and application appeal:
- Visual Comfort: Reduces eye strain in low-light conditions, improving usability for users working at night or in dim environments.
- Accessibility: Supports users with visual sensitivities, such as photophobia, aligning with accessibility standards, as discussed in [implementing accessibility in apps](/angular/accessibility/implement-a11y-in-app).
- Battery Savings: On OLED screens, dark mode consumes less power, extending battery life on mobile devices.
- User Preference: Many users prefer dark mode for aesthetic reasons, increasing engagement and satisfaction.
- Modern Design: Dark mode aligns with contemporary UI trends, giving applications a polished, professional look.
Angular’s component-based architecture and flexible styling system make it well-suited for implementing dark mode. By leveraging CSS custom properties or Angular Material’s theming capabilities, you can create a dynamic theming system that toggles between light and dark modes based on user preferences or system settings, ensuring a consistent and maintainable design.
Understanding Dark Mode Implementation in Angular
Implementing dark mode in Angular involves:
- CSS Custom Properties (CSS Variables): Define theme-specific styles (e.g., colors, backgrounds) that can be toggled at runtime, supported by all modern browsers.
- Angular Material Theming: Use SCSS to define light and dark themes for Material components, applying them via CSS classes.
- Theme Service: A service to manage theme state, toggle modes, and persist user preferences (e.g., in local storage).
- System Preferences: Detect the user’s operating system preference for dark mode using prefers-color-scheme.
- Component Styling: Apply theme styles to custom components, ensuring consistency across the application.
This guide covers two approaches: a lightweight solution using CSS custom properties for custom components and a robust solution using Angular Material for Material-based apps. Both approaches support dynamic theme switching and accessibility considerations.
Setting Up Your Angular Project for Dark Mode
Before implementing dark mode, configure your Angular project to support theming.
Step 1: Create or Verify Your Angular Project
If you don’t have a project, create one using the Angular CLI:
ng new dark-mode-app
cd dark-mode-app
Ensure the Angular CLI is installed:
npm install -g @angular/cli
Select SCSS as the stylesheet format for better style management:
ng new dark-mode-app --style=scss
Step 2: Add Dependencies (Optional)
Choose your theming approach:
- Angular Material: For apps using Material components, providing a pre-built theming system.
- No Framework: Use CSS custom properties for lightweight, custom theming.
Option A: Install Angular Material
Install Angular Material:
ng add @angular/material
During setup, choose:
- A pre-built theme (e.g., Indigo/Pink) as a starting point.
- Enable typography (optional).
- Enable animations for smooth transitions.
This adds @angular/material and @angular/cdk to your project. See using Angular Material for UI for details.
Option B: Use CSS Custom Properties
No additional dependencies are required for CSS-based theming, but ensure your project uses SCSS:
ng config schematics.@schematics/angular:component.style scss
Update angular.json if styles.css exists:
"styles": ["src/styles.scss"]
Step 3: Test the Setup
Run the application:
ng serve
Open http://localhost:4200 to confirm the app loads. You’re ready to implement dark mode.
Implementing Dark Mode with CSS Custom Properties
This approach is lightweight and ideal for custom components or non-Material apps.
Step 1: Define Light and Dark Themes
Edit src/styles.scss to define CSS custom properties for light and dark themes:
:root, .light-theme {
--primary-color: #3f51b5;
--accent-color: #ff4081;
--background-color: #ffffff;
--text-color: #333333;
--card-background: #f8f9fa;
}
.dark-theme {
--primary-color: #90caf9;
--accent-color: #ffd740;
--background-color: #121212;
--text-color: #ffffff;
--card-background: #1e1e1e;
}
body {
background: var(--background-color);
color: var(--text-color);
font-family: Arial, sans-serif;
margin: 0;
}
Breakdown:
- :root, .light-theme: Default light theme variables, applied unless overridden.
- .dark-theme: Dark theme variables, applied when the dark-theme class is active.
- Body: Uses variables for background and text color, ensuring global consistency.
Step 2: Create a Theme Service
Generate a service to manage theme state:
ng generate service theme
Edit src/app/theme.service.ts:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ThemeService {
private isDarkTheme = false;
constructor() {
// Detect system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
this.isDarkTheme = prefersDark;
this.applyTheme();
// Listen for system preference changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
this.isDarkTheme = e.matches;
this.applyTheme();
});
}
toggleTheme() {
this.isDarkTheme = !this.isDarkTheme;
this.applyTheme();
localStorage.setItem('theme', this.isDarkTheme ? 'dark' : 'light');
}
applyTheme() {
const theme = this.isDarkTheme ? 'dark-theme' : 'light-theme';
document.body.classList.remove('light-theme', 'dark-theme');
document.body.classList.add(theme);
}
isDarkMode(): boolean {
return this.isDarkTheme;
}
initializeTheme() {
// Load saved preference
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
this.isDarkTheme = savedTheme === 'dark';
this.applyTheme();
}
}
}
Key points:
- System Preference: Uses prefers-color-scheme to detect the user’s OS dark mode setting.
- Toggle Theme: Switches between light and dark modes, updating the body’s class.
- Persistence: Saves the user’s preference in localStorage.
- Initialization: Loads saved preferences on app startup.
Step 3: Create a Themed Component
Generate a component:
ng generate component dashboard
Edit src/app/dashboard/dashboard.component.html:
{ { themeService.isDarkMode() ? 'Switch to Light Mode' : 'Switch to Dark Mode' }}
Dashboard
Welcome to the themed dashboard, supporting light and dark modes.
Edit src/app/dashboard/dashboard.component.scss:
.container {
padding: 2rem;
}
.theme-toggle {
background: var(--primary-color);
color: var(--text-color);
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
margin-bottom: 1rem;
}
.card {
background: var(--card-background);
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
Edit src/app/dashboard/dashboard.component.ts:
import { Component, OnInit } from '@angular/core';
import { ThemeService } from '../theme.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent implements OnInit {
constructor(public themeService: ThemeService) {}
ngOnInit(): void {
this.themeService.initializeTheme();
}
}
Update app.component.html:
Step 4: Test Dark Mode
Run the app:
ng serve
Click the toggle button to switch between light and dark modes. The dashboard adapts instantly, with colors updating based on the active theme. Refresh the page to confirm the theme persists via localStorage. Test with your OS’s dark mode setting to verify prefers-color-scheme integration.
Implementing Dark Mode with Angular Material
For apps using Angular Material, leverage its theming system to apply light and dark themes to Material components.
Step 1: Define Light and Dark Themes
Create a theme file src/theme.scss:
@use '@angular/material' as mat;
$light-primary: mat.define-palette(mat.$indigo-palette, 500);
$light-accent: mat.define-palette(mat.$pink-palette, A200);
$light-warn: mat.define-palette(mat.$red-palette);
$dark-primary: mat.define-palette(mat.$blue-grey-palette, 500);
$dark-accent: mat.define-palette(mat.$amber-palette, A200);
$dark-warn: mat.define-palette(mat.$deep-orange-palette);
$light-theme: mat.define-light-theme((
color: (
primary: $light-primary,
accent: $light-accent,
warn: $light-warn
),
typography: mat.define-typography-config(),
density: 0
));
$dark-theme: mat.define-dark-theme((
color: (
primary: $dark-primary,
accent: $dark-accent,
warn: $dark-warn
),
typography: mat.define-typography-config(),
density: 0
));
.light-theme {
@include mat.all-component-themes($light-theme);
}
.dark-theme {
@include mat.all-component-themes($dark-theme);
}
Breakdown:
- Palettes: Define separate palettes for light and dark themes using Material’s color system.
- Themes: Create light-theme and dark-theme with distinct primary, accent, and warn colors.
- CSS Classes: Apply themes via .light-theme and .dark-theme classes, scoped to Material components.
Update src/styles.scss:
@use './theme';
Step 2: Update the Theme Service
Use the same theme.service.ts as above, ensuring it toggles between light-theme and dark-theme classes.
Step 3: Create a Material-Themed Component
Update dashboard.component.html:
Material Dashboard
{ { themeService.isDarkMode() ? 'light_mode' : 'dark_mode' }}
Dashboard
This dashboard uses Angular Material with light and dark themes.
Edit dashboard.component.scss:
mat-toolbar {
.spacer {
flex: 1;
}
}
.card {
margin: 2rem;
padding: 1.5rem;
}
Update app.module.ts to include Material modules:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard/dashboard.component';
@NgModule({
declarations: [AppComponent, DashboardComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
MatToolbarModule,
MatCardModule,
MatButtonModule,
MatIconModule
],
bootstrap: [AppComponent]
})
export class AppModule {}
Step 4: Test Material Dark Mode
Run the app and toggle the theme. Material components (toolbar, card, button) update seamlessly, reflecting the light or dark theme. Verify persistence and system preference detection as before.
Advanced Dark Mode Techniques
Supporting System Preference Changes
The ThemeService already listens for prefers-color-scheme changes. To enhance this, add a manual override option:
Update theme.service.ts:
export class ThemeService {
private isDarkTheme = false;
private useSystemTheme = true;
constructor() {
this.initializeTheme();
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (this.useSystemTheme) {
this.isDarkTheme = e.matches;
this.applyTheme();
}
});
}
setSystemTheme(useSystem: boolean) {
this.useSystemTheme = useSystem;
if (useSystem) {
this.isDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
this.applyTheme();
}
}
// ... rest of the service
}
Update dashboard.component.html:
Use System Theme
Import MatCheckboxModule in app.module.ts. This allows users to toggle system theme syncing.
Persisting Theme Across Routes
Ensure the theme persists when navigating routes. Use Angular’s router events:
Update app.component.ts:
import { Component, OnInit } from '@angular/core';
import { ThemeService } from './theme.service';
@Component({
selector: 'app-root',
template: ''
})
export class AppComponent implements OnInit {
constructor(private themeService: ThemeService) {}
ngOnInit() {
this.themeService.initializeTheme();
}
}
This ensures the theme is applied on app load, regardless of the route.
Optimizing for Accessibility
Enhance accessibility for dark mode:
- Contrast Ratios: Ensure text/background contrast meets WCAG 2.1 (4.5:1 for normal text). Use Chrome DevTools to verify.
- Reduced Motion: Respect prefers-reduced-motion for animations, as shown in [Angular animations](/angular/ui/angular-animations).
- ARIA Labels: Add ARIA attributes to theme toggle buttons, as discussed in [using ARIA labels in UI](/angular/accessibility/use-aria-labels-in-ui).
- Focus Indicators: Ensure focus styles are visible in both themes (e.g., outline or box-shadow).
Update dashboard.component.html:
{ { themeService.isDarkMode() ? 'light_mode' : 'dark_mode' }}
Theming Custom Components with Material
For custom components in a Material app, use Material’s typography and color utilities:
Update dashboard.component.scss:
@use '@angular/material' as mat;
.card {
background: mat.get-color-from-palette(mat.$background-palette, card);
color: mat.get-color-from-palette(mat.$foreground-palette, text);
}
This ensures custom components align with Material’s theme.
Testing Dark Mode
Test dark mode functionality and appearance:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { DashboardComponent } from './dashboard.component';
import { ThemeService } from '../theme.service';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture;
let themeService: ThemeService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DashboardComponent],
imports: [
BrowserAnimationsModule,
MatToolbarModule,
MatCardModule,
MatButtonModule,
MatIconModule
],
providers: [ThemeService]
}).compileComponents();
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
themeService = TestBed.inject(ThemeService);
fixture.detectChanges();
});
it('should toggle dark mode', () => {
spyOn(themeService, 'toggleTheme').and.callThrough();
const button = fixture.nativeElement.querySelector('button');
button.click();
fixture.detectChanges();
expect(themeService.toggleTheme).toHaveBeenCalled();
expect(themeService.isDarkMode()).toBe(true);
expect(document.body.classList.contains('dark-theme')).toBe(true);
});
});
For visual testing, use E2E tests with Cypress to verify theme styles across modes, as shown in creating E2E tests with Cypress. For testing setup, see using TestBed for testing.
Debugging Dark Mode
If dark mode doesn’t work, debug with:
- Theme Classes: Use Chrome DevTools (F12) to check if light-theme or dark-theme is applied to .
- CSS Variables: Inspect elements to verify --primary-color, --background-color, etc.
- Service Logic: Log themeService.isDarkMode() to trace theme state.
- SCSS Compilation: Ensure theme.scss is imported in styles.scss and angular.json.
- Browser Compatibility: Test in Chrome, Firefox, and Safari to rule out issues.
For general debugging, see debugging unit tests.
Optimizing Dark Mode Performance
To ensure dark mode is efficient:
- Minimize CSS: Use specific selectors to reduce CSS bloat, especially with Material’s all-component-themes.
- Lazy Load Themes: For large apps, load themes with feature modules, as shown in [creating feature modules](/angular/modules/create-feature-modules).
- Optimize Variables: Limit CSS custom properties to essential styles.
- Profile Performance: Use browser DevTools or Angular’s tools, as shown in [profiling app performance](/angular/performance/profile-app-performance).
Integrating Dark Mode into Your Workflow
To make dark mode seamless:
- Centralize Themes: Store theme definitions in theme.scss or a dedicated directory.
- Document Styles: Comment SCSS files to explain color roles and theme logic.
- Automate Testing: Include theme tests in CI/CD pipelines with ng test.
- Combine with UI Libraries: Use Angular Material or Tailwind CSS for themed components, as shown in [integrating Tailwind CSS](/angular/ui/integrate-tailwind-css).
- Support Accessibility: Ensure contrast and ARIA compliance, as discussed in [implementing accessibility in apps](/angular/accessibility/implement-a11y-in-app).
FAQ
What is dark mode in Angular?
Dark mode in Angular is a theming feature that toggles the application’s appearance between light and dark styles, using CSS custom properties or Angular Material theming, to enhance user comfort and accessibility.
Should I use Angular Material for dark mode?
Angular Material is ideal for apps using Material components, offering a robust theming system. For custom or lightweight apps, CSS custom properties provide flexibility without dependencies.
How do I detect system dark mode preferences?
Use the prefers-color-scheme media query with window.matchMedia to detect the user’s OS dark mode setting, as shown in the ThemeService.
How do I test dark mode?
Use unit tests with TestBed to verify theme toggling and E2E tests with Cypress for visual confirmation of styles. See creating E2E tests with Cypress.
Conclusion
Implementing dark mode in Angular applications enhances user experience, accessibility, and modern appeal. By using CSS custom properties for lightweight theming or Angular Material for robust component styling, you can create a dynamic theming system that supports light and dark modes, system preferences, and user persistence. This guide provides practical steps, from setup to advanced techniques like accessibility and performance optimization, ensuring your Angular apps are adaptable and professional. Start adding dark mode to your projects to deliver engaging, user-centric interfaces that meet contemporary design expectations.