Tokens
Noctis tokens are a layered graph that generates the CSS — every name and resolution chain derived, never hand-authored. Author against the graph instead of the cascade and the whole UI re-themes, re-sizes, and re-rounds from a handful of public inputs, with no value ever drifting from the token behind it. This guide is the mental model; the two tools at the end explore it with values read live from the running theme.
The six strata
The system layers six strata, from the consumer-facing seam down to the engine's private ramp. A token at any stratum resolves through the one below it, so the whole stack reduces to a handful of public inputs at the top and the OKLCH engine at the bottom.
| Stratum | What | CSS namespace | Visibility |
|---|---|---|---|
| Component | A part's recipe — the precompiled CSS a slot paints with, keyed off its data-slot | (consumes the tier below) | Public surface — what you render |
| Component token | The published per-component seam — anatomy-level knobs | --noctis-{component}-{anatomy}-{property}-{state} | Public — the override seam, shipped empty |
| Semantic | Intent roles — accent, border, surface, status | --noctis-color-{role} | Public read — overriding one is a retheme |
| Foundation | Derived scales the engine doesn't colour — type sizes, radius steps, spacing, motion, z-index | --noctis-{category}-{step} (--noctis-text-*, --noctis-radius-*, --noctis-space-*) | Public read |
| Seed | The public runtime inputs the scales and engine derive from | --noctis-seed-* (--noctis-seed-font-scale, --noctis-seed-radius, --noctis-seed-density) | Public — the runtime knobs |
| Engine primitive | The OKLCH ramp and its scope re-derivations — the private terminal stratum | --noctis-engine-* (--noctis-engine-bg-*, --noctis-engine-fg-*, --noctis-engine-control-*, --noctis-engine-el-* / --noctis-engine-mn-* / --noctis-engine-su-*) | Private |
A part doesn't paint with Tailwind utilities — it paints with precompiled CSS rules keyed off its data-slot, reading its own chain-carrying internals (a private --_{component}-{anatomy}-{property}-{state} namespace declared on the slot) plus the --noctis-* roles those internals resolve through. The internals read the public component token at the head of their chain and the semantic roles below it; both the rules and the internals are generated, never hand-authored.
Read it top-down and the stack is a single resolution chain: what you render at the top reduces, stratum by stratum, to the OKLCH engine at the bottom.
The bridge
Every public token carries one canonical name in the --noctis- namespace — --noctis-color-accent, --noctis-radius-md, --noctis-seed-density. That canonical name is the single source of truth; nothing else is hand-written.
Tailwind utilities are a generated bridge over those canonical names: bg-accent, rounded-md, text-large each resolve to their --noctis-* variable, so the utility vocabulary an author types never carries the namespace prefix and never drifts from the tokens behind it. The bridge is one-directional — change the canonical variable and every bridge utility that reads it moves; the utility names themselves don't change. bg-control-ghost-hover stays bg-control-ghost-hover whether or not the engine primitive behind it is re-derived.
A bridge comes in one of four kinds — color (a color utility), utility (a non-color spacing/size utility), theme (a Tailwind @theme key) and theme-inline (an inlined @theme key) — and the canonical name and its bridge are two spellings of one variable:
| Canonical name | Bridge utility | Both resolve to |
|---|---|---|
--noctis-color-accent | bg-accent | the live accent |
Breakpoints are the one exception that can't bridge through var() — a media-query prelude rejects it — so Noctis writes a literal theme value (--breakpoint-sm) instead of pointing it back at a --noctis-* variable.
Most semantic roles alias an engine primitive, but roughly 22 are authored, not derived: chart-1..8, avatar-1..10, text-disabled, and well are accent-independent and mode-split — re-theming the accent would break legend↔series identity, so they carry literal oklch() values on :root and under [data-theme="light"] rather than aliasing the ramp.
The seeds
Three public seed knobs sit at the surface, each a single value the foundation scales and the engine derive from, and each wired to a System Controls input so the whole UI re-sizes, re-rounds, or re-spaces live from one number:
--noctis-seed-font-scale— the Text Size slider. Every--noctis-text-*size resolves throughcalc(<base> * var(--noctis-seed-font-scale)), so one value resizes the entire type scale.--noctis-seed-radius— the Radius picker, a grid of border-onlyRadio.Cardpresets. Every--noctis-radius-*step derives from it, so one preset re-rounds everyrounded-*corner.--noctis-seed-density— the Density picker (default1), the sameRadio.Cardpreset model. Every spacing step resolves throughcalc(<base> * <step> * var(--noctis-seed-density)), so one preset re-spaces control heights, region padding, and the spacing scale together.
The radius cap
The radius foundation derives every box step from the radius seed but min()-caps each one, so surfaces never balloon into a pill no matter how round the seed goes. control is the one uncapped step — bare var(--noctis-seed-radius) — so at the default seed (9999px) controls go fully round while panels and cards stay bounded, and at seed 0 controls go square with the rest. full is a constant 9999px, the explicit pill.
| Step | Derivation | At seed 9999px | At seed 0 |
|---|---|---|---|
xs | min(seed × 0.5, 0.375rem) | 0.375rem (capped) | 0 |
sm | min(seed × 0.75, 0.5rem) | 0.5rem (capped) | 0 |
md | min(seed, 0.625rem) | 0.625rem (capped) | 0 |
lg | min(seed × 1.5, 0.875rem) | 0.875rem (capped) | 0 |
xl | min(seed × 2, 1.25rem) | 1.25rem (capped) | 0 |
control | var(--noctis-seed-radius) (uncapped) | 9999px (full round) | 0 (square) |
full | 9999px (constant) | 9999px | 9999px |
The naming grammar
A token name is a structured object; the CSS string is composed from its fields in a fixed order — component → anatomy → property → state — cssVarName reads token.category for the canonical segment, so the spelling falls out of the data rather than a hand-typed string:
The component segment is omitted for roles and scales. The anatomy segment is the part's data-slot name minus the component prefix (menu-item → item), and the root anatomy is omitted (--noctis-button-height). The property is always a real CSS property spelled out — background-color, padding-inline, border-radius — never an abbreviation. The state is one of a closed set (hover, active, focus, disabled, selected, highlighted, open, checked); the rest state carries no segment.
The resolution idiom
A public component token is defined by being consumed, never by being set. The internal variable carries the chain and is declared on the slot element — never on :root — so it re-resolves inside whatever elevation or radius scope the slot sits in:
/* The declaration — internal aliases the public token, falling back to a raw default. */
[data-slot="noctis-menu-item"] {
--_menu-item-height: var(--noctis-menu-item-height, 2rem);
}
/* The consumption — the precompiled rule reads only the internal. */
[data-slot="noctis-menu-item"] {
min-block-size: var(--_menu-item-height);
}The mint default is a raw 2rem literal, not an alias to another scale; the precompiled rule consumes only the internal variable, never the public name. The public --noctis-menu-item-height sits unset at the head of the chain — the component reads the fallback until a consumer sets it. The component tier ships empty: every public --noctis-* token resolves to its default until a consumer claims it, and one consumer line always wins everywhere. The same internal variable re-resolves under each elevation scope, so a token reads a different value inside a menu than at the root without any extra declaration.
The override ladder
The rungs reach from the broadest retheme to the narrowest one-off, each inheriting into everything below its scope:
- Theme seed —
{ background, accent, contrast }regenerates the entire engine ramp and every role that derives from it. - Engine override — re-point one
--noctis-engine-*primitive. A generation-time override (generateTheme(seed, { overrides })/ThemeProvider overrides) replaces the primitive before derivation, so every dependent re-derives; a cascade override is an unlayered rule on the emitted variable that wins by cascade but re-derives nothing. - Role override — set a
--noctis-color-{role}to re-point one semantic role across the whole system. - Component token — set a public
--noctis-{component}-…token to reskin one anatomy part wherever that component renders in scope. - Slot CSS — target a
[data-slot]for a single bespoke surface.
The Customization cookbook walks each rung with worked examples.
The tools
Two full-screen tools explore the same graph, with every value read live from the running theme — drag Contrast in System Controls and they re-paint in place. The full accessible reference is the viewer table.