ThemeService
ThemeService is the runtime theming engine for Synapse UI. It injects CSS custom property maps onto document.body, enabling instant theme switching without page reload or recompilation.
Installation
npm install @synapse-ui/theme
Basic Usage
import { Component, inject } from '@angular/core';
import { ThemeService } from '@synapse-ui/theme';
@Component({
selector: 'app-root',
standalone: true,
template: `
<button (click)="toggle()">
Switch to {{ themeService.theme() === 'light' ? 'dark' : 'light' }}
</button>
`,
})
export class AppComponent {
readonly themeService = inject(ThemeService);
toggle(): void {
const next = this.themeService.theme() === 'light' ? 'dark' : 'light';
this.themeService.setTheme(next);
}
}
Bootstrap
Provide the service at application root. During Phase 1 implementation, the exact provider token will be documented here. Expected pattern:
import { ApplicationConfig } from '@angular/core';
import { provideSynapseTheme } from '@synapse-ui/theme';
export const appConfig: ApplicationConfig = {
providers: [
provideSynapseTheme({ defaultTheme: 'light', persist: true }),
],
};
API Reference (Planned)
theme(): Signal<SynapseThemeName>
Read-only signal of the active theme name.
Type: 'light' | 'dark' | 'high-contrast'
setTheme(name: SynapseThemeName): void
Applies the named theme by setting CSS custom properties on document.body.
toggleTheme(): void
Cycles through available themes: light → dark → high-contrast → light.
setCustomTokens(tokens: Partial<SynapseTokenMap>): void
Merges custom token overrides on top of the active theme. Useful for brand customization without forking token files.
resetCustomTokens(): void
Removes custom overrides and restores the base theme map.
availableThemes(): readonly SynapseThemeName[]
Returns the list of built-in theme names.
Persistence
When persist: true is set in config, the selected theme is saved to localStorage under the key synapse-ui-theme and restored on next load.
System Preference Detection
On first visit (no stored preference), ThemeService checks prefers-color-scheme:
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Defaults to 'dark' if true, 'light' otherwise
High-contrast mode is never auto-selected — it must be explicitly chosen by the user.
How It Works
flowchart LR
A[setTheme called] --> B[Load token map for theme]
B --> C[Merge custom overrides]
C --> D[Apply CSS vars to document.body]
D --> E[Update theme signal]
E --> F[Persist to localStorage]
Each token in the map becomes an inline style on body:
<body style="--synapse-surface-primary: #0F172A; --synapse-text-primary: #F1F5F9; ...">
Components reference these variables in their scoped CSS — no JavaScript needed per component.
Storybook Integration
Provide a global Storybook decorator that wraps stories with theme switching:
// .storybook/preview.ts (planned)
export const decorators = [
(story, context) => {
// ThemeService wrapper with toolbar global
},
];
Storybook toolbar will expose Light / Dark / High-Contrast toggles.
Accessibility Notes
- High-contrast theme uses maximum contrast pairings (black/white/yellow).
- Focus ring tokens are theme-aware and meet WCAG 2.1 AA contrast requirements.
- Theme changes do not trigger page reload; use
aria-live="polite"on theme toggle buttons to announce changes.