Skip to main content

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: lightdarkhigh-contrastlight.

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.