Design Skins
Thirteen switchable visual designs — eleven token-only color skins plus two full designs (Bento Studio and Console) that also swap fonts, radius and the home dashboard.
Overview
Skins are a headline selling point of Luminaux: the entire app can be re-colored (and, for two of them, re-typeset and re-laid-out) instantly, with the choice persisted and applied before first paint so there's never a flash of the wrong design.
Because design tokens are the only place colors live, a skin is nothing
more than a block of token overrides scoped under [data-skin="…"] on <html>. Switch the attribute and
the whole UI re-skins.
Color skins (token-only)
Default, Aqua, Lime, Lilac, Prism, Midnight, Ocean, Graphite, Emerald, Amber, Coral — they redefine the raw color tokens and nothing else, as variants of the standard layout.
Full designs
Bento Studio (bento) and Console (console). In addition to colors they swap fonts and card radius, and render a bespoke home dashboard.
Every skin defines both a light and a dark variant, so skin and light/dark mode are orthogonal.
Architecture & files
| File | Responsibility |
|---|---|
src/styles/index.css | Defines the default palette, scoped to :root, [data-skin='default'] (+ .dark, [data-skin='default'].dark). Also the @theme inline mapping and skin-aware .font-display/.font-data helpers. |
src/styles/_skin-colors.scss | All eleven color skins except default — one [data-skin="x"] + one [data-skin="x"].dark block each (raw --* token overrides only). |
src/styles/_skin-bento.scss | Bento Studio: warm palette, big radius (--radius-card: 1.5rem), tile-tint tokens, flattened shadows. |
src/styles/_skin-console.scss | Console: terminal palette, monospace --font-*, tight radius (0.25rem). |
src/styles/_skin-prism.scss | Prism: neutral base + categorical --c1..--c6, applied to .stat-icon / .chart-bar marker classes. |
src/config/skins.ts | The Skin union and the SKINS array — the registry, kept free of React imports so build tooling can read it too. |
src/context/LayoutContext.tsx | skin/setSkin, <html data-skin> reflection, and localStorage('design_skin') persistence. Re-exports SKINS/Skin, so @/context/LayoutContext stays the import site. |
index.html | Pre-paint script with the skin allowlist — applies data-skin before paint (falls back to default for unknown values). |
src/layout/overlays/LayoutCustomizer.tsx | The "Design" skin picker; maps over SKINS, labels via customizer:skin<Cap>Label/Hint. |
src/layout/DesignSystemMenu.tsx | Header palette-icon quick switcher with live swatches. |
src/pages/HomeRoute.tsx | The home branch — renders BentoDashboard for bento, ConsoleDashboard for console, else CrmDashboard. |
src/locales/en/customizer.json | Per-skin skin<Cap>Label / skin<Cap>Hint strings. |
Mechanism at a glance: setSkin → React state → an effect writes <html data-skin> and
localStorage('design_skin') → the scoped SCSS block wins → the app re-skins. On reload, the pre-paint
script re-applies data-skin before paint.
Usage
For non-developers. A skin can be switched from four places, all sharing one source of truth:
- Layout Customizer (header gear → "Design" section) — cards with a label + hint.
- Layout Settings page (
/layout-settings) — the full-page twin of the Customizer. - ⌘K command palette — searchable skin entries.
- Header DesignSystemMenu (palette icon) — quick switch with live color swatches.
The choice is remembered across reloads and sessions.
For developers. Read and set the skin from the layout context:
import {useLayout, SKINS, type Skin} from '@/context/LayoutContext'
function SkinPicker() {
const {skin, setSkin} = useLayout()
return (
<div className="flex gap-2">
{SKINS.map((id) => (
<button
key={id}
onClick={() => setSkin(id)}
className={id === skin ? 'ring-2 ring-ring' : ''}
>
{id}
</button>
))}
</div>
)
}The Skin type & SKINS list
From src/context/LayoutContext.tsx:
export type Skin =
| 'default' | 'bento' | 'aqua' | 'lime' | 'lilac' | 'prism'
| 'midnight' | 'ocean' | 'graphite' | 'emerald' | 'amber' | 'coral' | 'console'
export const SKINS: Skin[] = [
'default', 'aqua', 'lime', 'lilac', 'prism', 'midnight',
'ocean', 'graphite', 'emerald', 'amber', 'coral', 'console', 'bento',
]Display order
SKINS defines the display order used by the Customizer, Layout Settings and ⌘K palette (it differs
slightly from the type's declaration order — bento is listed last). DEFAULT_SKIN is 'default'.
Skin catalog
data-skin | Label / hint (i18n) | Kind | Character | Notes |
|---|---|---|---|---|
default | Default · Indigo · clean | Color | Clean blue SaaS | Baseline palette in index.css. |
aqua | Aqua · Teal · cyan | Color | Teal / cyan | |
lime | Lime · Sky · lime | Color | Sky + vivid lime | Uses dark --primary-foreground (#0b0f0a) for contrast on lime. |
lilac | Lilac · Lavender | Color | Lavender / mauve | |
prism | Prism · Multicolor | Color | Violet chrome + multicolor data | Categorical --c1..--c6; see below. |
midnight | Midnight · Slate · indigo | Color | Slate-indigo | |
ocean | Ocean · Azure · blue | Color | Azure / blue | |
graphite | Graphite · Mono · minimal | Color | Mono near-black / light-gray | info stays blue for signal. |
emerald | Emerald · green | Color | Green | |
amber | Amber · gold | Color | Gold | Uses dark --primary-foreground (#1f1500) for contrast on amber. |
coral | Coral · warm | Color | Salmon / warm | |
console | Console · Dev · terminal | Design | Terminal green, JetBrains Mono, tight radius 0.25rem | Renders ConsoleDashboard. |
bento | Bento Studio · Warm · bento | Design | Warm tangerine, bento-grid, big radius 1.5rem | Keeps default font; renders BentoDashboard. |
Each color skin overrides the standard token set (--background, --surface, --surface-muted,
--foreground, --muted-foreground, --border, --primary, --primary-hover, --primary-foreground,
--ring, and typically --info) under [data-skin="x"] and again under [data-skin="x"].dark.
Layout context skin API
From useLayout():
| Member | Type | Description |
|---|---|---|
skin | Skin | The active skin. |
setSkin(s) | (Skin) => void | Switch skin; an effect reflects <html data-skin> and persists localStorage('design_skin'). |
Persistence key: localStorage('design_skin'). Invalid/unknown values fall back to 'default' (both at
load time in loadSkin() and in the pre-paint allowlist).
Prism categorical palette
Prism keeps chrome on a single violet --primary but colors data elements with a six-hue categorical
palette (src/styles/_skin-prism.scss):
| Var | Light | Dark |
|---|---|---|
--c1 | #7c3aed (violet) | #a78bfa |
--c2 | #ec4899 (pink) | #f472b6 |
--c3 | #f97316 (orange) | #fb923c |
--c4 | #14b8a6 (teal) | #2dd4bf |
--c5 | #3b82f6 (blue) | #60a5fa |
--c6 | #f59e0b (amber) | #fbbf24 |
Applied via marker classes: .stat-grid > * .stat-icon (each card a different hue, cycled 4n) and
.chart-bar (cycles the full palette 6n). These marker classes are harmless no-ops under every other
skin.
Adding a skin = 5 spots
To ship a new color skin (say sunset), touch exactly these five places:
Add the SCSS block
In src/styles/_skin-colors.scss add a [data-skin="sunset"] block and a [data-skin="sunset"].dark
block, each redefining the 11 raw --* tokens (use raw --*, never --color-*).
Extend the type & list
In src/config/skins.ts add 'sunset' to the Skin union and to the SKINS array (its
position sets the display order).
Confirm the picker
src/layout/overlays/LayoutCustomizer.tsx maps over SKINS, so it picks the new skin up automatically;
just make sure its label keys exist (step 5). (Historically this was a separate skinIds list; the
current customizer iterates SKINS directly.)
Add to the pre-paint allowlist
In index.html add 'sunset' to the pre-paint skins allowlist array so it applies before paint
(otherwise it falls back to default on reload).
Add the labels
In src/locales/en/customizer.json add skinSunsetLabel and skinSunsetHint strings (the
Customizer/Settings/⌘K read customizer:skin<Cap>Label/Hint).
Layout Settings, the ⌘K palette and the header DesignSystemMenu then pick it up automatically from SKINS.
Adding a full "design"
A full design (like Bento or Console) does everything a color skin does plus:
- Its own
_skin-*.scsspartial with font/radius overrides (e.g.--font-sans/-display/-mono,--radius-card, extra tokens like Bento's tile tints). Bento also softens.shadow-smand maps.rounded-xl/.rounded-lgto its larger radius. - A branch in
HomeRoute()(src/pages/HomeRoute.tsx) to render its bespoke dashboard:
export function HomeRoute() {
const {skin} = useLayout()
if (skin === 'bento') return <BentoDashboard/>
if (skin === 'console') return <ConsoleDashboard/>
return <CrmDashboard/>
}Bento tiles live in src/components/bento/; the Console dashboard is
src/pages/dashboards/ConsoleDashboard.tsx.
Examples
A skin's SCSS block (raw tokens, light + dark) — the shape you copy for a new color skin:
/* src/styles/_skin-colors.scss */
[data-skin='ocean'] {
--background: #f0f6fc;
--surface: #ffffff;
--surface-muted: #e6f0f9;
--foreground: #0c2030;
--muted-foreground: #51687a;
--border: #d7e6f1;
--primary: #0a84ff;
--primary-hover: #0a6fd6;
--primary-foreground: #ffffff;
--ring: #0a84ff;
--info: #22a7f0;
}
[data-skin='ocean'].dark {
--background: #04121f;
--surface: #0a1f30;
/* …the dark counterparts… */
--primary: #3aa0ff;
--primary-foreground: #04121f;
}A full-design partial adds fonts + radius (Console):
/* src/styles/_skin-console.scss */
[data-skin='console'] {
--primary: #15803d;
--font-sans: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
--font-display: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
--radius-card: 0.25rem;
}Rendering a per-skin swatch preview (scope a nested data-skin so it shows that skin's colors regardless of
the active one):
<div data-skin="emerald" className="bg-surface border border-border p-3 rounded-lg">
<span className="inline-block h-4 w-4 rounded-full bg-primary"/>
</div>Best practices
- Skin = token overrides only. Keep color skins purely to
--*values; don't add layout/behavior. Reserve font/radius/dashboard changes for genuine full "designs". - Always use raw
--*tokens in the SCSS, never--color-*(those are Tailwind aliases that resolve transparent in plain CSS). See Design Tokens & CSS. - Define both light and dark for every skin — they're orthogonal to the theme mode.
- Watch
--primary-foregroundcontrast on light/vivid primaries. Lime and Amber deliberately use a dark--primary-foregroundso text on the accent stays legible. - Keep the allowlist in sync. Every skin id must appear in the
index.htmlpre-paint array, or it won't survive a reload (it falls back todefault). - Register persisted keys.
design_skinis already handled by the "Reset to defaults" flow (src/lib/appStorage.ts); any new skin-related key must be added there too.
Troubleshooting
| Symptom | Likely cause & fix |
|---|---|
| New skin reverts to default after reload. | Its id isn't in the index.html pre-paint skins allowlist. Add it. |
| New skin doesn't appear in the picker. | Missing from SKINS in LayoutContext, or its customizer:skin<Cap>Label key is absent. |
| Skin colors look transparent / broken in SCSS. | You used var(--color-*). Use raw var(--surface) etc. |
| Text on the accent is unreadable (light primary). | Set a dark --primary-foreground for that skin (as Lime/Amber do). |
| Prism multicolor isn't showing. | The data element is missing its marker class (.stat-icon inside .stat-grid, or .chart-bar). |
| Full design's dashboard doesn't change. | Add the skin === 'x' branch in HomeRoute() (src/pages/HomeRoute.tsx). |
| Nested "default" swatch inherits the active skin. | Ensure the default palette stays scoped to :root, [data-skin='default'] (it is, in index.css); wrap the swatch in data-skin="default". |
FAQ
How many skins ship? Thirteen: eleven color skins (including Default) plus two full designs (Bento Studio, Console).
Is a skin the same as dark mode? No. Skin (color identity) and mode (light/dark) are independent; every skin defines both light and dark. See Design Tokens & CSS.
Do color skins change the layout? No — they're pure token overrides. Only the two full designs also swap fonts, radius and the home dashboard.
Why do Lime and Amber use dark text on the accent? Their --primary is a bright yellow/gold; white text
would fail contrast, so --primary-foreground is a near-black.
Where does the skin persist? localStorage('design_skin'), reflected on <html data-skin> and
re-applied pre-paint from index.html.
Can OLED combine with a skin? Yes — OLED deepens the dark neutrals while the skin's --primary accent is
preserved.
Notes for designers & content editors
- To design a new skin, you only pick eleven colors × two modes (light + dark). Match the token roles:
canvas (
--background), cards (--surface), recessed (--surface-muted), text (--foreground/--muted-foreground), lines (--border), accent (--primary+ hover + foreground), focus (--ring), and--info. - Verify the accent contrast: whatever sits on a
--primaryfill uses--primary-foreground. If the accent is light, make that foreground dark. - Bento and Console are more than palettes — they change type and layout. Treat them as distinct product looks, not just color themes.
- Labels and one-line hints for each skin are editable in
src/locales/en/customizer.json(skin<Name>Label/skin<Name>Hint) and translated per language.
Related
Design Tokens & CSS
The token system every skin is built on.
Customizer & Settings
The Design section skin picker and reset flow.
Layout & Shell
The DesignSystemMenu palette-icon quick switcher with live swatches.
Motion & Effects
The Bento/Console dashboards and skin-aware chart tokens.
Architecture
Project structure and the routing model.
Was this page helpful?
