PVR Tech Studio
Css system

Design Tokens & CSS

How color, type and radius are defined once as CSS custom properties, mapped into Tailwind v4 utilities, and switched between Light / Dark / System modes plus true-black OLED.

8 min read
Updated July 15, 2026

Overview

Every color in Luminaux lives in exactly one place: CSS custom properties (design tokens). Components never reference a raw hex value or a raw Tailwind palette color (like bg-slate-800). Instead they use a small set of semantic utilities — bg-surface, text-foreground, border-border, bg-primary, and so on — that resolve to whichever token is currently active.

Why this matters:

  • Light and dark stay in sync automatically. Because a component only says "use the surface color", flipping to dark mode (or any skin) just swaps the token value — no component edits.
  • Skins are free. A whole re-color of the app (see Design Skins) is nothing more than a block of token overrides.
  • One source of truth. A designer can retune the entire product palette by editing a handful of variables in src/styles/index.css.

Dark mode is a proper Light / Dark / System mode (not a bare on/off toggle). Light is the default on first load; dark only applies when the user chooses it, or chooses "System" while their OS is in dark mode. An optional OLED (true-black) variant deepens dark mode to pure black for OLED screens.

Architecture & files

FileResponsibility
src/styles/index.cssThe Tailwind entry (@import 'tailwindcss'). Defines the raw tokens on :root (light) and .dark (dark), the OLED block, the @theme inline mapping into Tailwind utilities, and the CJK font overrides.
src/hooks/useTheme.tsThe Light/Dark/System mode store (a useSyncExternalStore module store). Persists to localStorage('theme'), toggles .dark on <html>, follows the OS in system mode.
src/context/LayoutContext.tsxOwns the oledDark flag (persisted localStorage('oled_dark')), reflected on <html data-oled>; also owns skins (see sibling doc).
index.htmlPre-paint inline script that applies the saved theme (.dark), skin (data-skin), OLED (data-oled) and language (lang) before first paint so there is no flash of the wrong design.
src/styles/_scrollbar.scssThin, theme/skin-aware scrollbar referencing the raw --border / --muted-foreground tokens.

Key dependencies: Tailwind CSS v4 via @tailwindcss/vite (CSS-first config — there is no tailwind.config.js); React 19.

How the layers connect

index.html (pre-paint)      →  sets .dark / data-skin / data-oled / lang on <html>
        │
src/styles/index.css        →  :root + .dark define raw --* tokens
        │  @theme inline
        ▼
Tailwind utilities          →  bg-surface, text-foreground, border-border, …
        │
Components                  →  use only those semantic utilities

useTheme() and LayoutContext mutate the classes/attributes on <html> at runtime; the pre-paint script mirrors the same logic so the first paint already matches.

Usage

For non-developers. The theme mode is switched from three places, all kept in sync:

  • The header theme control (top bar).
  • The Layout Customizer (gear icon → Appearance) — a Light / Dark / System segmented control.
  • The Layout Settings page (/layout-settings).

OLED true-black is a switch in the Customizer's Appearance section (only meaningful while in dark mode).

For developers. Style with the semantic utilities and read the theme from the shared hook:

import {useTheme} from '@/hooks/useTheme'
 
function Example() {
    const {mode, theme, setMode, toggleTheme} = useTheme()
    // mode  = user's choice: 'light' | 'dark' | 'system'
    // theme = resolved value:  'light' | 'dark'
    return (
        <div className="bg-surface text-foreground border border-border rounded-xl p-4">
            <p className="text-muted-foreground">Current mode: {mode}</p>
            <button className="bg-primary text-primary-foreground px-3 py-1.5 rounded-lg" onClick={toggleTheme}>
                Toggle
            </button>
        </div>
    )
}

Semantic color tokens

Defined as raw CSS custom properties. Light lives on :root, [data-skin='default']; dark on .dark, [data-skin='default'].dark. (The [data-skin='default'] mirror keeps a nested "default" swatch — e.g. the Layout Settings skin picker — on baseline blue even when another skin is active on <html>.)

TokenLight (:root)Dark (.dark)Meaning
--background#f6f7f9#0b1120App canvas behind cards
--surface#ffffff#111827Card / panel surface
--surface-muted#f1f5f9#1e293bRecessed surface (inputs, chips)
--foreground#0f172a#e5e7ebPrimary text
--muted-foreground#64748b#94a3b8Secondary / hint text
--border#e5e8ee#1f2937Hairline borders, dividers
--primary#0067ff#2b7fffBrand / action color
--primary-hover#0058db#4d96ffHover state of primary
--primary-foreground#ffffff#ffffffText/icon on a primary fill
--success#16a34a#4ade80Positive tone
--warning#d97706#fbbf24Caution tone
--danger#dc2626#f87171Destructive / error tone
--info#0ea5e9#38bdf8Informational tone
--ring#0067ff#2b7fffFocus ring color

Tailwind utility classes

@theme inline in index.css maps each raw token to a --color-* alias so Tailwind generates matching utilities:

@theme inline {
    --color-surface: var(--surface);
    --color-foreground: var(--foreground);
    --color-primary: var(--primary);
    /* …one per token… */
}

That yields the utility families you should reach for:

Utility (representative)Backed by token
bg-background--background
bg-surface / bg-surface-muted--surface / --surface-muted
text-foreground / text-muted-foreground--foreground / --muted-foreground
border-border--border
bg-primary / text-primary / text-primary-foreground / bg-primary-hover--primary*
bg-success / text-success (and warning / danger / info)--success etc.
ring-ring--ring

Type & radius tokens

TokenValue (default)Notes
--font-sans'Plus Jakarta Sans', 'Inter', …Set in @theme inline; the body font.
--font-display'Inter', …Skin-aware heading face; read by .font-display.
--font-monoui-monospace, 'JetBrains Mono', …Read by .font-data (tabular numerals).
--radius-card0.75remCard radius; skins override (Bento 1.5rem, Console 0.25rem).

The @layer components block in index.css exposes two skin-aware helpers:

.font-display { font-family: var(--font-display); }
.font-data    { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

CJK languages swap only the font families (via html[lang='ja'] / html[lang='zh'] overriding --font-sans/-display/-mono to Noto Sans JP / SC) — all other languages keep the default faces.

useTheme() return value

src/hooks/useTheme.ts is a module-level useSyncExternalStore so the header, Customizer and Layout Settings all read/write one shared value.

MemberTypeDescription
theme'light' | 'dark'The resolved appearance (what .dark reflects).
mode'light' | 'dark' | 'system'The user's choice. system follows the OS via matchMedia.
setMode(next)(ThemeMode) => voidSet the mode; persists to localStorage('theme'), toggles .dark, notifies subscribers.
setTheme(next)(Theme) => voidBack-compat alias — delegates to setMode.
toggleTheme()() => voidFlip between light and dark based on the resolved theme.

Persistence key: localStorage('theme') = 'light' | 'dark' | 'system'. In system mode the store re-resolves on OS theme changes.

Layout context (OLED)

From useLayout() in src/context/LayoutContext.tsx:

MemberTypeDescription
oledDarkbooleanTrue-black variant of dark mode.
setOledDark(v)(boolean) => voidToggle it; persists to localStorage('oled_dark'), reflects <html data-oled="true">.

The OLED override block in index.css deepens the neutral tokens (keeping each skin's --primary):

html.dark[data-oled='true'],
.dark.sidebar-oled {
    --background: #000000;
    --surface: #0a0a0a;
    --surface-muted: #161616;
    --foreground: #ededed;
    --muted-foreground: #8a8a8a;
    --border: #242424;
}

The same block also serves .dark.sidebar-oled — a true-black sidebar alone on an otherwise-lighter page (see the layout system's oledSidebar flag).

Configuration & customization

Retune the whole palette: edit the token values in the :root, [data-skin='default'] (light) and .dark, [data-skin='default'].dark (dark) blocks of src/styles/index.css. Every component updates automatically.

Add a new semantic token: (1) add the raw --myToken to both the light and dark blocks; (2) add --color-myToken: var(--myToken); inside @theme inline; (3) use it via bg-myToken / text-myToken in components (or var(--myToken) in SCSS).

Change the OLED palette: edit the html.dark[data-oled='true'], .dark.sidebar-oled block.

Change fonts: update --font-sans (in @theme inline), and --font-display / --font-mono on :root. Skins may override these — see the sibling doc.

Examples

A themable card that adapts to light/dark/skins/OLED with zero conditional logic:

export function StatTile({label, value}: {label: string; value: string}) {
    return (
        <div className="bg-surface border border-border rounded-xl shadow-sm p-4">
            <p className="text-sm text-muted-foreground">{label}</p>
            <p className="font-data text-2xl text-foreground">{value}</p>
            <span className="mt-2 inline-block text-xs text-success">▲ trending up</span>
        </div>
    )
}

Correct SCSS token usage (raw var, not the alias):

/* GOOD — resolves to the live token */
.my-panel {
    background: var(--surface);
    border: 1px solid var(--border);
    color: var(--foreground);
}
 
/* BAD — --color-surface is transparent in plain CSS */
.my-panel {
    background: var(--color-surface); /* wrong */
}

The pre-paint script (already in index.html) that prevents a flash:

<script>
    ;(function () {
        try {
            var t = localStorage.getItem('theme')
            if (t === 'dark' || (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
                document.documentElement.classList.add('dark')
            }
            if (localStorage.getItem('oled_dark') === 'true') {
                document.documentElement.dataset.oled = 'true'
            }
        } catch (e) {}
    })()
</script>

Best practices

  • Never hardcode a hex value in a component, and never use raw Tailwind palette colors (bg-slate-800, text-zinc-500) for themable surfaces. Add or reuse a token instead.
  • In SCSS, always use the raw token (var(--surface)), never var(--color-*).
  • Read theme from useTheme() — it's a shared store. Don't re-implement a per-component useState toggle; that desyncs the header from the Customizer.
  • Prefer semantic tokens over literal ones (bg-surface over a specific gray) so skins keep working.
  • Register any new persisted key (if you add a theme-related localStorage key) in src/lib/appStorage.ts so "Reset to defaults" clears it. theme and oled_dark are already covered.

Troubleshooting

SymptomLikely cause & fix
A SCSS background comes out transparent.You used var(--color-surface). Switch to the raw var(--surface)@theme inline aliases aren't real runtime vars.
Flash of the wrong theme on load.The pre-paint script in index.html was removed or the key name changed. It must run before paint and read localStorage('theme') / 'oled_dark'.
Header toggle and Customizer show different modes.Something replaced useTheme's module store with local useState. Keep the shared useSyncExternalStore store.
Dark mode auto-applies on first visit.useTheme defaults to 'light'; check the pre-paint script isn't adding .dark unconditionally. Dark should apply only for 'dark', or 'system' + OS dark.
OLED does nothing.OLED only deepens dark mode. Ensure the app is in dark (or System→dark) and data-oled="true" is on <html>.
A color didn't update in dark mode.The component used a literal color instead of a token utility. Replace it with bg-*/text-*/border-* semantic classes.

FAQ

Is dark mode a toggle or a mode? A mode — Light / Dark / System. system tracks the OS live via matchMedia.

What's the default on a fresh browser? Light. Dark never auto-applies unless explicitly chosen (or System + OS dark).

Where is tailwind.config.js? There isn't one. Tailwind v4 is configured CSS-first via @theme inline in src/styles/index.css.

Does OLED change the accent color? No — it only deepens the neutral surfaces/background; each skin keeps its own --primary.

Can I use tokens in inline styles? Yes: style={{ background: 'var(--surface)' }} works because the raw tokens are real CSS variables (unlike the --color-* aliases).

Notes for designers & content editors

  • Think in roles, not colors: "surface", "muted text", "primary action", "danger". Pick the token that matches the role and light/dark are handled for you.
  • To reskin the product, you rarely touch components — you edit token values. See Design Skins for shipping a whole new palette.
  • Contrast pairs are intentional: text on --surface uses --foreground; text/icons on a --primary fill use --primary-foreground. Keep those pairings when tuning values.
  • .font-display (headings) and .font-data (numbers/metrics, tabular) are the two type helpers; numbers align in columns because .font-data uses tabular numerals.

Was this page helpful?