Runtime API
The Runtime API allows you to change the theme dynamically in the browser. This is useful for:
- Theme Builders: Letting users customize their UI.
- White Labeling: Loading a brand’s colors from an API at runtime.
- User Preferences: Allowing users to tweak contrast or saturation.
The Reactive Pipeline (Engine)
The core logic lives in css/engine.css. It uses CSS @property to create a reactive data flow that responds instantly to JavaScript changes.
1. Inputs (Variables)
The engine listens to specific CSS variables that you can set on the :root or any element.
--base-hue: The primary brand hue (0-360).--base-chroma: The saturation level (0-0.25).--surface-token: The lightness value (usually set by the static CSS, but can be overridden).
2. Computation (The Engine)
The engine calculates intermediate values and combines them into the final color.
/* Simplified Engine Logic */
.surface-card {
/* Calculate Chroma based on the base chroma */
--computed-surface-C: calc(var(--base-chroma) * 0.5);
/* Calculate Hue (potentially shifted) */
--computed-surface-H: var(--base-hue);
/* Combine into final color */
--computed-surface: oklch(
var(--surface-token) var(--computed-surface-C) var(--computed-surface-H)
);
}
3. Output (Properties)
The computed colors are assigned to standard CSS properties.
.surface-card {
background-color: var(--computed-surface);
color: var(--computed-fg-color);
}
Animation Strategy
The runtime supports smooth animations for both continuous and discrete changes.
Continuous Changes (Hue/Chroma)
When you animate --base-hue (e.g., from Blue to Red), the browser interpolates the number, and the engine recalculates the color every frame. This is “free” because of the reactive pipeline.
Discrete Changes (Light/Dark Mode)
When light-dark() flips from Light to Dark, the input token (--surface-token) changes instantly. Normally, this would cause a harsh snap.
The Fix: We transition the Computed Properties, not the inputs.
/* css/engine.css */
@property --computed-surface {
syntax: "<color>";
inherits: true;
initial-value: transparent;
}
* {
/* The browser interpolates the RESULT of the calculation */
transition: --computed-surface 0.2s;
}
By registering --computed-surface with @property, we tell the browser it’s a color. When the input snaps, the result changes, and the browser transitions smoothly between the old result and the new result.
Browser Integration
The library provides a ThemeManager class to manage the theme mode and sync it with the browser’s native UI (Address Bar, Favicon).
ThemeManager
The ThemeManager handles:
- Mode Switching: Toggling between
light,dark, andsystemmodes. - DOM Updates: Applying the correct classes or
color-schemestyles to the root element. - Browser Sync: Automatically updating the address bar color and favicon.
import { ThemeManager } from "color-system/browser";
// Initialize the manager
const themeManager = new ThemeManager({
// Optional: Custom classes for light/dark modes
lightClass: "light-theme",
darkClass: "dark-theme",
// Optional: Generator for dynamic favicons
faviconGenerator: (color) => `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="14" fill="${color}" />
</svg>
`,
});
// Set the mode
themeManager.setMode("dark"); // Force dark mode
themeManager.setMode("light"); // Force light mode
themeManager.setMode("system"); // Follow system preference
// Get the current resolved mode ('light' or 'dark')
console.log(themeManager.resolvedMode);
// Clean up listeners when done
themeManager.dispose();
Automatic Syncing
When you call setMode(), the ThemeManager automatically:
- Updates the root element (e.g.
document.documentElement). - Waits for styles to compute.
- Updates
<meta name="theme-color">to match the body background. - Updates the favicon (if a generator is provided) to match the body text color.