Token-driven theming
Lumora ships 39 themes built from 41 CSS tokens. Switch the active theme with a single attribute. Override tokens to brand-match in minutes.
The token model
Every Lumora component reads its surface, color, radius, shadow, and motion values from CSS variables prefixed --lm-*. A theme is a complete assignment of values to those variables.
- Color: surfaces (bg, surface, surface-raised, surface-sunken), text, borders, and 7 semantic accents (primary, secondary, accent, success, warning, danger, info).
- Radius: 5 steps from
sm(0.25rem) to2xl(1.5rem). - Shadow: 5 elevation steps including a
glowvariant for primary actions. - Motion: spring + ease-out curves and 3 duration tokens. Auto-disabled under
prefers-reduced-motion. - Density: a single multiplier that scales control padding and gaps.
Derived tokens
color-mix at the CSS layer — for example --lm-color-primary-soft tracks --lm-color-primary automatically across every theme. You only need to set the base color.Switching themes
Set data-lm-theme on any ancestor of your components. Most apps put it on <html>.
// Anywhere in your app
document.documentElement.dataset.lmTheme = "lumora-dark";Here's a typed React toggle that persists to localStorage:
// app/components/ThemeToggle.tsx
"use client";
import { useEffect, useState } from "react";
export function ThemeToggle() {
const [theme, setTheme] = useState("lumora-light");
useEffect(() => {
document.documentElement.dataset.lmTheme = theme;
localStorage.setItem("theme", theme);
}, [theme]);
return (
<select
value={theme}
onChange={(e) => setTheme(e.target.value)}
className="lm-select"
>
<option value="lumora-light">Light</option>
<option value="lumora-dark">Dark</option>
<option value="indigo-enterprise">Indigo</option>
</select>
);
}Live preview
Pick a theme to apply it to the entire docs site — including this page. The choice persists across navigation and reloads.
Theme switcher
Driven by data-lm-theme on <html>.
Tokens cascade through every component
Buttons, alerts, focus rings, and shadows all derive from the active theme.
Avoiding the dark→light flash
If you persist user theme choice in localStorage, inject this 6-line script in <head>. It runs before paint, so the user never sees a flash of the wrong theme.
<!-- app/layout.html — inject in <head> -->
<script>
(function() {
try {
var t = localStorage.getItem('theme');
if (t) document.documentElement.setAttribute('data-lm-theme', t);
} catch (e) {}
})();
</script>Building a custom theme
A theme is a plain object that satisfies the LumoraTheme type. Fork an existing one or build from scratch — the contract is 41 tokens.
// my-app/lumora-themes.ts
import type { LumoraTheme } from "@lumora-design/themes";
export const acmeTheme: LumoraTheme = {
name: "acme",
label: "Acme Corp",
mode: "light",
tokens: {
"color-bg": "#fafaf9",
"color-surface": "#ffffff",
"color-surface-raised": "#f5f5f4",
"color-surface-sunken": "#f0f0ef",
"color-text": "#1c1917",
"color-muted": "#57534e",
"color-border": "#e7e5e4",
"color-border-strong": "#d6d3d1",
"color-primary": "#dc2626", // Acme red
"color-primary-fg": "#ffffff",
"color-primary-soft": "color-mix(in oklab, var(--lm-color-primary) 14%, var(--lm-color-surface))",
"color-secondary": "#1c1917",
"color-secondary-fg": "#ffffff",
"color-accent": "#0891b2",
"color-accent-fg": "#ffffff",
"color-success": "#15803d",
"color-success-fg": "#ffffff",
"color-warning": "#a16207",
"color-warning-fg": "#ffffff",
"color-danger": "#b91c1c",
"color-danger-fg": "#ffffff",
"color-info": "#0369a1",
"color-info-fg": "#ffffff",
"color-overlay": "rgb(28 25 23 / 0.55)",
"color-focus-ring": "color-mix(in oklab, var(--lm-color-primary) 35%, transparent)",
"radius-sm": "0.25rem",
"radius-md": "0.5rem",
"radius-lg": "0.75rem",
"radius-xl": "1rem",
"radius-2xl": "1.5rem",
"shadow-sm": "0 1px 2px rgb(0 0 0 / 0.06)",
"shadow-md": "0 4px 12px -2px rgb(0 0 0 / 0.08)",
"shadow-lg": "0 12px 32px -8px rgb(0 0 0 / 0.16)",
"shadow-xl": "0 24px 64px -12px rgb(0 0 0 / 0.22)",
"shadow-glow": "0 0 0 1px rgb(220 38 38 / 0.18), 0 8px 24px -6px rgb(220 38 38 / 0.32)",
"ease-out": "cubic-bezier(0.22, 1, 0.36, 1)",
"ease-spring": "cubic-bezier(0.34, 1.56, 0.64, 1)",
"duration-fast": "120ms",
"duration-base": "180ms",
"duration-slow": "260ms",
density: "1"
}
};Pass it to the plugin via the themes option. Pass defaultTheme to set the boot fallback.
/* app/globals.css */
@import "tailwindcss";
@plugin "@lumora-design/core" {
themes: ["lumora-light", "lumora-dark", "acme"];
defaultTheme: "acme";
}WCAG AA, automatically
color-bg / color-text, color-primary / color-primary-fg, and 8 other pairs at AA across every theme. Run pnpm test to validate custom themes too.Per-tenant theming
For multi-tenant SaaS, set data-lm-theme on a wrapping element instead of <html>. Lumora components inside the wrapper read the nearest ancestor — branding switches per region without forking your component code.
// app/page.tsx
export default async function TenantPage({ params }) {
const tenant = await getTenant(params.tenant);
return (
<div data-lm-theme={tenant.themeName}>
{/* every Lumora component inside picks up the tenant brand */}
</div>
);
}Density modes
Density scales control padding and gap proportionally. Use it for compact data tables, enterprise admin dense screens, or generous marketing pages — without adjusting any spacing classes.
<!-- Per-page density -->
<body data-lm-density="compact">
<!-- All controls render 12% more compact -->
</body>
<!-- Per-region density -->
<section class="lm-density-spacious">
<!-- This region renders 14% more spacious -->
</section>Dense tables and admin views
Default for app shells
Marketing and onboarding
All 39 built-in themes
Every theme below is a complete drop-in. Click a swatch to copy the data-lm-theme value.
Light · 19 themes
data-lm-theme="lumora-light"data-lm-theme="slate-boardroom"data-lm-theme="cobalt-office"data-lm-theme="emerald-ledger"data-lm-theme="indigo-enterprise"data-lm-theme="rose-compliance"data-lm-theme="amber-ops"data-lm-theme="teal-systems"data-lm-theme="graphite-command"data-lm-theme="violet-suite"data-lm-theme="sky-analytics"data-lm-theme="neutral-density"data-lm-theme="sunset"data-lm-theme="mint"data-lm-theme="berry"data-lm-theme="ocean"data-lm-theme="mocha"data-lm-theme="pastel"data-lm-theme="solar"Dark · 20 themes
data-lm-theme="lumora-dark"data-lm-theme="slate-boardroom-dark"data-lm-theme="cobalt-office-dark"data-lm-theme="emerald-ledger-dark"data-lm-theme="indigo-enterprise-dark"data-lm-theme="rose-compliance-dark"data-lm-theme="amber-ops-dark"data-lm-theme="teal-systems-dark"data-lm-theme="graphite-command-dark"data-lm-theme="violet-suite-dark"data-lm-theme="sky-analytics-dark"data-lm-theme="neutral-density-dark"data-lm-theme="sunset-dark"data-lm-theme="mint-dark"data-lm-theme="berry-dark"data-lm-theme="ocean-dark"data-lm-theme="mocha-dark"data-lm-theme="pastel-dark"data-lm-theme="carbon"data-lm-theme="aurora-dark"