Modern Theme Management for Angular - A lightweight, feature-rich theme library with automatic dark mode detection, SSR-safe, and zero configuration required.
- 🎨 Automatic Theme Detection - Supports light, dark, and system themes with OS preference detection
- ⚡ Angular 20 Signals - Built with modern Angular signals for optimal performance and reactivity
- 🖥️ SSR-safe - No hydration mismatch, works with Angular SSR out of the box
- 🎯 Zero Configuration - Works out of the box with sensible defaults
- 🔧 Flexible Strategy - Choose between class-based or attribute-based theming
- 📦 Tiny Bundle - Lightweight with no unnecessary dependencies
- 🛡️ Production Ready - Comprehensive error handling and memory leak prevention
- ♿ Accessibility Friendly - Respects user preferences and system settings
- 🚀 Performance Optimized - Efficient DOM updates and minimal re-renders
- 🔒 Type Safe - Full TypeScript support with strict type checking
- 🧪 Tested - Comprehensive test coverage for reliability
- 📚 Well Documented - Extensive documentation with real-world examples
- ⚙️ Modern Architecture - Uses Angular's app initializer for clean, testable initialization
npm install @slateui/themeAdd the theme provider to your app.config.ts:
import{ApplicationConfig}from'@angular/core';import{provideSlateUiTheme}from'@slateui/theme';exportconstappConfig: ApplicationConfig={providers: [provideSlateUiTheme()]};import{Component,inject}from'@angular/core';import{ThemeService}from'@slateui/theme';
@Component({selector: 'app-header',template: ` <header> <h1>My App</h1> <button (click)="toggleTheme()">Toggle Theme</button> <p>Current theme: {{ themeService.theme() }}</p> <p>Resolved theme: {{ themeService.resolvedTheme() }}</p> </header> `})exportclassHeaderComponent{privatethemeService=inject(ThemeService);toggleTheme(){this.themeService.toggle();}}/* Default styles (light theme) */:root {
--bg-color:#ffffff;
--text-color:#000000;
--primary-color:#3b82f6;
}
/* Dark theme styles */
.dark {
--bg-color:#1f2937;
--text-color:#f9fafb;
--primary-color:#60a5fa;
}
body {
background-color:var(--bg-color);
color:var(--text-color);
transition: background-color 0.3s ease, color 0.3s ease;
}Add this inline script to your index.html<head>:
<script>(function(){'use strict';try{vart=localStorage.getItem('theme')||'system',e=t==='system'?window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light':t==='light'||t==='dark'?t:'light',n=document.documentElement;if(n){n.classList.remove('light','dark'),e==='dark'?(n.classList.add('dark'),n.setAttribute('data-theme','dark')):(n.classList.remove('dark'),n.removeAttribute('data-theme')),n.style.colorScheme=e}}catch(e){try{varn=document.documentElement;n&&(n.classList.remove('light','dark'),n.removeAttribute('data-theme'),n.style.colorScheme='light')}catch(e){}}})();</script>Why inline? Angular does not provide a way to inject scripts into the HTML <head> at build time. For true FOUC prevention, the script must run immediately as the HTML is parsed—before any content is rendered. External scripts or Angular providers/services run too late to prevent a flash. This is why the script must be copied directly into your index.html head.
Note: This approach is SSR-safe: the initial HTML uses the default theme, and the correct theme is applied instantly on page load.
- The SSR HTML always uses the default theme, since user preferences are only available in the browser.
- The inline script applies the correct theme instantly on page load, so users never see a flash of the wrong theme.
- This is the standard, SSR-safe approach used by modern theme libraries (like next-themes).
- Native Angular integration: signals, DI, and standalone components
- TypeScript-first and future-proof (Angular 20+ ready)
- Clean, testable architecture (app initializer pattern)
- Consistent, standardized theming across apps
- Excellent developer experience (autocomplete, IDE support)
- Performance optimized and tree-shakeable
- Well-documented, maintainable, and enterprise-ready
@slateui/theme uses Angular's provideAppInitializer() for clean, testable initialization:
// Traditional approach (other libraries)constructor(){this.initialize();// Side effects in constructor}// @slateui/theme approachprovideAppInitializer(()=>{constthemeService=inject(ThemeService);themeService.initialize();// Clean, controlled initializationreturnPromise.resolve();})- 🔄 Testable - Can test service without auto-initialization
- ⚡ Performant - No constructor side effects
- 🎯 Controlled - Can conditionally initialize based on app state
- 🧹 Clean - Separation of concerns
- 🔧 Flexible - Manual initialization when needed
- 📚 Modern - Follows Angular 20+ best practices
interfaceThemeConfig{defaultTheme?: 'light'|'dark'|'system';// Default: 'system'storageKey?: string;// Default: 'theme'strategy?: 'attribute'|'class';// Default: 'attribute'enableAutoInit?: boolean;// Default: trueenableColorScheme?: boolean;// Default: trueenableSystem?: boolean;// Default: trueforcedTheme?: 'light'|'dark'|'system';// Default: undefined}provideSlateUiTheme({strategy: 'class'})provideSlateUiTheme({storageKey: 'my-app-theme'})provideSlateUiTheme({enableSystem: false})provideSlateUiTheme({forcedTheme: 'dark'})The main service that manages theme state using Angular signals.
theme()- Readonly signal for current theme settingsystemTheme()- Readonly signal for system theme preferenceresolvedTheme()- Computed signal for the actual applied themeinitialized- Boolean property indicating if service is initializedisForced- Boolean property indicating if forced theme is active
setTheme(theme: 'light' | 'dark' | 'system')- Set the themetoggle()- Cycle through themes (light → dark → system)isDark()- Check if current theme is darkisLight()- Check if current theme is lightisSystem()- Check if using system themegetConfig()- Get current configurationcleanup()- Manual cleanup (automatically called on destroy)
import{Component,inject}from'@angular/core';import{ThemeService}from'@slateui/theme';
@Component({selector: 'app-example',template: ` <div> <h1>Theme Demo</h1> <div class="theme-info"> <p>Current setting: {{ themeService.theme() }}</p> <p>System preference: {{ themeService.systemTheme() }}</p> <p>Applied theme: {{ themeService.resolvedTheme() }}</p> <p>Is dark mode: {{ themeService.isDark() ? 'Yes' : 'No' }}</p> </div> <div class="theme-controls"> <button (click)="themeService.setTheme('light')">Light</button> <button (click)="themeService.setTheme('dark')">Dark</button> <button (click)="themeService.setTheme('system')">System</button> <button (click)="themeService.toggle()">Toggle</button> </div> </div> `})exportclassExampleComponent{privatethemeService=inject(ThemeService);}The ThemeService automatically handles cleanup when the application is destroyed. However, you can also manually manage the lifecycle:
import{Component,inject,OnDestroy}from'@angular/core';import{ThemeService}from'@slateui/theme';
@Component({selector: 'app-example',template: `...`})exportclassExampleComponentimplementsOnDestroy{privatethemeService=inject(ThemeService);ngOnDestroy(){// Manual cleanup (optional - automatic cleanup is handled)this.themeService.cleanup();}}// Get current configurationconstconfig=this.themeService.getConfig();console.log('Current config:',config);provideSlateUiTheme({strategy: 'class'})/* CSS */
.dark {
--bg-color:#1f2937;
--text-color:#f9fafb;
}<!-- HTML --><htmlclass="dark"><!-- Dark theme applied --></html>provideSlateUiTheme({strategy: 'attribute'})/* CSS */
[data-theme="dark"] {
--bg-color:#1f2937;
--text-color:#f9fafb;
}<!-- HTML --><htmldata-theme="dark"><!-- Dark theme applied --></html>The package automatically handles SSR scenarios:
- Server-side rendering - Uses default values for consistent rendering
- Hydration safety - Prevents mismatches between server and client
- Client-side activation - Loads saved preferences and applies them
- No additional configuration needed for Angular SSR
provideSlateUiTheme({enableAutoInit: false})// In your componentexportclassAppComponentimplementsOnInit{privatethemeService=inject(ThemeService);ngOnInit(){// Initialize when readythis.themeService.initialize();}}provideSlateUiTheme({enableAutoInit: false})// Initialize based on conditionsngOnInit(){if(this.shouldInitializeTheme()){this.themeService.initialize();}}import{effect,inject}from'@angular/core';import{ThemeService}from'@slateui/theme';// Listen to theme changeseffect(()=>{constthemeService=inject(ThemeService);consttheme=themeService.resolvedTheme();console.log('Theme changed to:',theme);// Apply custom logicif(theme==='dark'){// Dark theme specific logic}});- Core package: ~13KB (raw) / ~3KB (gzipped)
- Zero external dependencies - Only Angular core and common
- Tree-shakeable - Unused features are removed
Contributions are welcome! To contribute:
- Fork this repository.
- Create a new branch for your feature or fix.
- Make your changes and ensure all tests pass.
- Open a Pull Request with a clear description of your changes.
Please review our Contributing Guide before submitting your PR.
MIT License - see LICENSE file for details.
- Inspired by next-themes
- Built with Angular
Made with ❤️ for the Angular community
Created by @immohammadjaved