theme-config
v1.0.3
Published
One theme engine. Light, dark, and unlimited custom themes.
Maintainers
Readme
theme-config
CSS-variable theme engine for any framework. Light, dark, and unlimited custom themes.
Created by Muhammad
Most theme libraries give you a dark mode toggle. theme-config gives you a full theme engine — unlimited named themes, automatic persistence, OS preference detection, cross-tab sync, smooth transitions, and a clean runtime API. So you can stop re-inventing this wheel.
Installation
npm install theme-config
# or
yarn add theme-config
# or
pnpm add theme-configRequirements: Any modern browser. Zero runtime dependencies.
Features
- Unlimited themes —
light,dark,red,blue, or anything you define - CSS variables — themes are maps of custom properties applied directly to
<html> - Persistence — survives page refreshes via
localStorageorsessionStorage - System preference — respects
prefers-color-schemeand watches for OS changes live - Cross-tab sync — theme changes instantly reflect in every open tab
- Dynamic themes — add or remove themes at runtime with
.add()and.remove() - Transitions — smooth CSS transitions between theme switches
- Event system — subscribe to theme changes with
.onChange() - Zero dependencies — no runtime deps, ~4 kB gzipped
Quick start
import { createTheme } from "theme-config";
const theme = createTheme({
themes: {
light: {
"--primary": "#000000",
"--background": "#ffffff",
"--foreground": "#111111",
"--border": "#e5e5e5",
},
dark: {
"--primary": "#ffffff",
"--background": "#111111",
"--foreground": "#ffffff",
"--border": "#333333",
},
},
defaultTheme: "light",
persist: true,
});
// Switch theme
theme.set("dark");
// Toggle light ↔ dark
theme.toggle();Your CSS just uses the variables — no extra work:
body {
background: var(--background);
color: var(--foreground);
}
button {
background: var(--primary);
border: 1px solid var(--border);
}How it works
When you call createTheme():
- All theme definitions are stored in an internal registry
- The last saved theme is read from storage (or
defaultThemeis used) - That theme's CSS variables are applied directly to
<html>viaelement.style.setProperty() - The active theme name is set as
<html data-theme="dark">(or a class, your choice)
Every call to .set(), .toggle(), .next() or .previous() runs through the same pipeline:
applyTheme(name)
│
├── resolve "system" → "light" or "dark" via matchMedia
├── look up variables in registry
├── remove old CSS vars from <html>
├── apply new CSS vars to <html>
├── update data-theme attribute
├── save to localStorage
├── broadcast to other tabs (if sync: true)
└── fire onChange callbacksAPI
createTheme(config)
Creates and initializes a theme instance. Returns a ThemeInstance.
const theme = createTheme(config: ThemeConfig): ThemeInstanceConfig options
themes — required
A record of theme names to theme definitions.
themes: {
light: { "--primary": "#000", "--background": "#fff" },
dark: { "--primary": "#fff", "--background": "#111" },
}Optionally include display metadata for building theme pickers:
themes: {
red: {
label: "Ruby", // human-readable name
color: "#ef4444", // swatch color
variables: {
"--primary": "#ef4444",
"--background": "#fffafa",
},
},
}defaultTheme
Theme applied on first load when nothing is saved in storage.
defaultTheme: "light" // defaults to first key in themespersist
Remember the active theme across page loads.
persist: true // default: truestorage
Where to persist the theme.
storage: "localStorage" // default
storage: "sessionStorage" // tab only
storage: "none" // no persistencestorageKey
Key used in storage.
storageKey: "my-app-theme" // default: "theme-config"attribute
How the active theme name is applied to the DOM element.
attribute: "data-theme" // default → <html data-theme="dark">
attribute: "class" // → <html class="theme-dark">classPrefix
Prefix used when attribute: "class".
classPrefix: "theme-" // default → "theme-dark"
classPrefix: "app-" // → "app-dark"target
DOM element to apply variables and the attribute to.
target: document.documentElement // default (<html>)
target: document.getElementById("app")!transition
Smoothly animate between theme switches.
transition: true // default: false
transitionDuration: 300 // ms, default: 200sync
Synchronize the active theme across all open browser tabs.
sync: true // default: falseInstance methods
.set(name, options?)
Switch to a named theme. Pass "system" to follow OS preference.
theme.set("dark")
theme.set("red")
theme.set("system") // follows prefers-color-scheme
theme.set("dark", { transition: true }) // per-call transition override.toggle()
Toggle between light and dark.
theme.toggle()
// light → dark → light → ....next()
Advance to the next theme in registration order. Wraps around.
theme.next()
// light → dark → red → blue → light → ....previous()
Go to the previous theme. Wraps around.
theme.previous().current()
Returns the currently active theme name.
theme.current() // "dark" | "red" | "system" | ....getThemes()
Returns all registered theme names in order.
theme.getThemes()
// ["light", "dark", "red", "blue"].getThemeMeta()
Returns full metadata for all themes. Useful for building theme pickers.
theme.getThemeMeta()
// [
// { name: "light", variables: { "--primary": "..." } },
// { name: "red", label: "Ruby", color: "#ef4444", variables: { ... } },
// ].has(name)
Returns true if the theme is registered.
theme.has("red") // true
theme.has("purple") // false.add(name, definition)
Register a new theme at runtime.
theme.add("purple", {
"--primary": "#a855f7",
"--background": "#faf5ff",
"--foreground": "#1a0a2e",
"--border": "#e9d5ff",
})
theme.set("purple").remove(name)
Remove a registered theme. If it was active, falls back to defaultTheme.
theme.remove("purple").onChange(callback)
Subscribe to theme changes. Returns an unsubscribe function.
const unsubscribe = theme.onChange((newTheme, previousTheme) => {
console.log(`${previousTheme} → ${newTheme}`)
})
// Stop listening:
unsubscribe().destroy()
Tear down all listeners, BroadcastChannel, and callbacks. Call when unmounting.
theme.destroy()Recipes
Auto-generate a theme picker
const meta = theme.getThemeMeta();
meta.forEach(({ name, label, color }) => {
const btn = document.createElement("button");
btn.textContent = label ?? name;
if (color) btn.style.setProperty("--swatch", color);
btn.onclick = () => theme.set(name);
picker.appendChild(btn);
});System preference with manual override
const theme = createTheme({
themes: { light: {...}, dark: {...} },
defaultTheme: "system",
});
theme.set("dark"); // user overrides OS setting
theme.set("system"); // back to following OSReact integration
import { createTheme } from "theme-config";
import { useState, useEffect } from "react";
const theme = createTheme({ themes: { light: {...}, dark: {...} } });
export function useTheme() {
const [current, setCurrent] = useState(theme.current());
useEffect(() => {
return theme.onChange((newTheme) => setCurrent(newTheme));
}, []);
return {
theme: current,
setTheme: (name) => theme.set(name),
toggleTheme: () => theme.toggle(),
themes: theme.getThemes(),
};
}Preventing flash on page load
Add this inline script in your <head> before any other scripts or styles:
<script>
(function () {
try {
var key = "theme-config";
var saved = localStorage.getItem(key);
var system = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
document.documentElement.setAttribute("data-theme", saved || system);
} catch (e) {}
})();
</script>This synchronously applies the theme attribute before the browser renders anything, eliminating the white flash between page load and JS execution.
Multi-tenant / per-user themes
const theme = createTheme({
themes: {
"brand-a": { "--primary": "#e63946", "--background": "#f1faee" },
"brand-b": { "--primary": "#2196f3", "--background": "#e3f2fd" },
},
storageKey: `theme-${userId}`,
persist: true,
});TypeScript
Fully typed. All types are exported:
import type {
ThemeConfig,
ThemeInstance,
ThemeDefinition,
ThemeVariables,
ThemeMeta,
NormalizedTheme,
SetOptions,
ThemeChangeCallback,
UnsubscribeFn,
StorageType,
AttributeMode,
} from "theme-config";Architecture
theme-config
│
├── src/
│ ├── index.ts Public re-exports
│ └── core/
│ ├── types.ts All TypeScript types and interfaces
│ ├── createTheme.ts Main factory — wires everything together
│ ├── storage.ts localStorage / sessionStorage wrapper
│ ├── events.ts Typed event emitter (Set-based)
│ └── system.ts prefers-color-scheme wrapper
│
└── dist/
├── theme-config.js ESM output
├── theme-config.cjs CommonJS output
└── index.d.ts Bundled type declarationsLicense
MIT © Muhammad
Contributing
Issues and PRs welcome at github.com/unfathomable-08/theme-config.
