Brands
How a brand is defined, registered, and activated — and why swapping one is a single import.
A brand is one CSS file that assigns values to every name in the
token contract. The default brand owns :root;
every other brand scopes itself to a [data-brand] attribute.
:root {
/* 1. primitives — the only layer allowed product vocabulary */
--n-0: #ececec; /* canvas */
--n-900: #0a0a0a; /* ink */
--sc1m-orange: #ff5a00; /* CTAs, focus, the brand voice */
--sc1m-paper: #ffffff; /* page and card fills */
/* 2. semantics — fixed names, mapped from the primitives above */
--accent: var(--sc1m-orange);
--bg: var(--sc1m-paper);
--ring: var(--sc1m-orange);
}[data-brand="luntian"] {
/* same section order, same names, different values */
}Every brand file keeps the same section order — primitives → surfaces & text → interactive → status → shape & type — so the files read as mirrors of one another and a diff between two brands is meaningful.
Activating a brand
Set the attribute on <html>, so portalled popups (which render into <body>)
rebrand along with everything else:
<html data-brand="luntian">The default brand carries no attribute — it owns :root, and is also the
fallback for any token another brand skips.
Adding a brand
Copy the template
src/styles/brands/boilerplate-template.css is the canonical boilerplate.
Copy it to src/styles/brands/<brand>.css and replace every BRAND with your
brand key and every <value> with a real value.
Assign every token
Any token you skip silently falls back to the default brand. That is a convenience, not a strategy — the parity test will tell you which you missed.
Register it in three places
- the
@importinsrc/app/globals.css— after the default brand, because order decides the fallback BRANDSinsrc/styles/contract.ts- the Brand toolbar items in
.storybook/preview.tsx
Verify
npx vitest --project storybook run src/styles/tokens.stories.tsxRules of the contract
- Brand files assign bare semantic names (
--accent); the@themeblock maps them to utilities. Never rename a semantic name. - Product and domain vocabulary lives only in section 1 primitives —
Luntian's
--money-inis legal there and nowhere else. - For dark mode, flip the primitives, then override only the semantics that genuinely change. Comment the reason.
Optional slots you can skip freely: --brand-shadow-raised,
--brand-shadow-overlay. Type scale, spacing/density and motion durations are
reserved — likely future widenings of the contract, so coordinate rather than
self-serve.