Theming
Stratum’s theme system is built on two core ideas: every component reads from a typed AppTheme interface (never from hardcoded values), and all themes are registered in a single THEME_REGISTRY that everything else derives from automatically.
The AppTheme interface
Section titled “The AppTheme interface”AppTheme is defined in src/tokens/theme-protocol.ts. Every theme — built-in or custom — must implement this interface. Key sections:
interface AppTheme { colors: { // Backgrounds background: string; surface: string; surfaceRaised: string; // Text textPrimary: string; textSecondary: string; textMuted: string; textInverse: string; textOnAccent: string; // Accent (one hue — ADR-010) accent: string; accentHover: string; accentSubtle: string; // Signal success: string; successSubtle: string; warning: string; warningSubtle: string; error: string; errorSubtle: string; // Borders + overlay border: string; borderStrong: string; borderSubtle: string; overlay: string; }; typography: { fontFamily: { heading: string; body: string; mono: string } }; radius: { none: number; sm: number; md: number; lg: number; xl: number; full: number }; border: { thin: number; medium: number; strong: number }; shadow: { none: object; sm: object; md: object; lg: object }; iconWeight: IconWeight; darkIconWeight?: IconWeight; buttonShape: 'default' | 'pill'; effects: { blurIntensity: number; glassTint: string; glowColor: string; innerBorderColor: string };}Built-in themes
Section titled “Built-in themes”| Theme | buttonShape | iconWeight | radius.md | border.thin |
|---|---|---|---|---|
| Slate light/dark | pill | regular | 8 | 1 |
| Obsidian light/dark | default | bold | 0 | 2 |
| Quartz light/dark | pill | light | 8 | 1 |
Theme registry
Section titled “Theme registry”src/tokens/themes/registry.ts is the single source of truth:
export const THEME_REGISTRY: ThemeEntry[] = [ { name: 'slate', label: 'Slate', light: slateLight, dark: slateDark }, { name: 'obsidian', label: 'Obsidian', light: obsidianLight, dark: obsidianDark }, { name: 'quartz', label: 'Quartz', light: quartzLight, dark: quartzDark },];Unistyles, ThemeProvider, and all TypeScript types auto-derive from this registry. Adding a new theme is two steps: create the file, add one entry.
Creating a custom theme
Section titled “Creating a custom theme”Use createTheme() to deep-merge overrides onto a base theme:
import { createTheme } from '@/tokens/themes/createTheme';import { slateThemeLight, slateThemeDark } from '@/tokens/themes/slate';
export const brandLight = createTheme(slateThemeLight, { colors: { accent: '#E11D48', accentHover: '#BE123C', accentSubtle: '#FFF1F2', },});
export const brandDark = createTheme(slateThemeDark, { colors: { accent: '#FB7185', accentHover: '#F43F5E', accentSubtle: '#4C0519', },});Then register it:
import { brandLight, brandDark } from './brand';
export const THEME_REGISTRY = [ // ... existing themes { name: 'brand', label: 'Brand', light: brandLight, dark: brandDark },];That’s it — setTheme('brand') now works.
Switching themes at runtime
Section titled “Switching themes at runtime”import { useThemeToggle } from '@/hooks/useThemeToggle';
const { themeName, colorScheme, setTheme, toggleColorScheme } = useThemeToggle();Theme switching triggers zero re-renders in components — Unistyles observes the registry change and updates styles in place.
Related
Section titled “Related”- Colors — semantic color slots reference
- Spacing — spacing scale
- Typography — type scale