Skip to content
Documentation menu
Theming · 8 min

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) to 2xl (1.5rem).
  • Shadow: 5 elevation steps including a glow variant 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

Some tokens are derived via 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.ts
// Anywhere in your app
document.documentElement.dataset.lmTheme = "lumora-dark";

Here's a typed React toggle that persists to localStorage:

app/components/ThemeToggle.tsx
// 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
<!-- 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
// 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
/* app/globals.css */
@import "tailwindcss";
@plugin "@lumora-design/core" {
  themes: ["lumora-light", "lumora-dark", "acme"];
  defaultTheme: "acme";
}

WCAG AA, automatically

Lumora ships a contrast test that verifies 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
// 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.

density.htmlhtml
<!-- 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>
compact0.88×

Dense tables and admin views

comfortable1.00×

Default for app shells

spacious1.14×

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

Lumora Lightlight
data-lm-theme="lumora-light"
Slate Boardroomlight
data-lm-theme="slate-boardroom"
Cobalt Officelight
data-lm-theme="cobalt-office"
Emerald Ledgerlight
data-lm-theme="emerald-ledger"
Indigo Enterpriselight
data-lm-theme="indigo-enterprise"
Rose Compliancelight
data-lm-theme="rose-compliance"
Amber Opslight
data-lm-theme="amber-ops"
Teal Systemslight
data-lm-theme="teal-systems"
Graphite Commandlight
data-lm-theme="graphite-command"
Violet Suitelight
data-lm-theme="violet-suite"
Sky Analyticslight
data-lm-theme="sky-analytics"
Neutral Densitylight
data-lm-theme="neutral-density"
Sunsetlight
data-lm-theme="sunset"
Mintlight
data-lm-theme="mint"
Berrylight
data-lm-theme="berry"
Oceanlight
data-lm-theme="ocean"
Mochalight
data-lm-theme="mocha"
Pastellight
data-lm-theme="pastel"
Solarlight
data-lm-theme="solar"

Dark · 20 themes

Lumora Darkdark
data-lm-theme="lumora-dark"
Slate Boardroom Darkdark
data-lm-theme="slate-boardroom-dark"
Cobalt Office Darkdark
data-lm-theme="cobalt-office-dark"
Emerald Ledger Darkdark
data-lm-theme="emerald-ledger-dark"
Indigo Enterprise Darkdark
data-lm-theme="indigo-enterprise-dark"
Rose Compliance Darkdark
data-lm-theme="rose-compliance-dark"
Amber Ops Darkdark
data-lm-theme="amber-ops-dark"
Teal Systems Darkdark
data-lm-theme="teal-systems-dark"
Graphite Command Darkdark
data-lm-theme="graphite-command-dark"
Violet Suite Darkdark
data-lm-theme="violet-suite-dark"
Sky Analytics Darkdark
data-lm-theme="sky-analytics-dark"
Neutral Density Darkdark
data-lm-theme="neutral-density-dark"
Sunset Darkdark
data-lm-theme="sunset-dark"
Mint Darkdark
data-lm-theme="mint-dark"
Berry Darkdark
data-lm-theme="berry-dark"
Ocean Darkdark
data-lm-theme="ocean-dark"
Mocha Darkdark
data-lm-theme="mocha-dark"
Pastel Darkdark
data-lm-theme="pastel-dark"
Carbondark
data-lm-theme="carbon"
Auroradark
data-lm-theme="aurora-dark"