Noctis

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.

StratumWhatCSS namespaceVisibility
ComponentA 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 tokenThe published per-component seam — anatomy-level knobs--noctis-{component}-{anatomy}-{property}-{state}Public — the override seam, shipped empty
SemanticIntent roles — accent, border, surface, status--noctis-color-{role}Public read — overriding one is a retheme
FoundationDerived 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
SeedThe 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 primitiveThe 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.

How a token resolvesA value at any stratum resolves through the one below it — the recipe reads a component token, which falls back to a semantic role, down to a foundation scale, the seeds you tune, and finally the engine's private ramp.
USER
Component recipeprecompiled CSS · what you render
USER
Component token--noctis-{component}-* · override seam, shipped empty
PLATFORM
Semantic role--noctis-color-{role} · intent
PLATFORM
Foundation scale--noctis-{category}-* · type, space, radius…
EXTERNAL
Seed--noctis-seed-* · the runtime knobs
NEUTRAL
Engine primitive--noctis-engine-* · private OKLCH ramp
A value at any stratum resolves through the one below it — the recipe reads a component token, which falls back to a semantic role, down to a foundation scale, the seeds you tune, and finally the engine's private ramp.

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 nameBridge utilityBoth resolve to
--noctis-color-accentbg-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 through calc(<base> * var(--noctis-seed-font-scale)), so one value resizes the entire type scale.
  • --noctis-seed-radius — the Radius picker, a grid of border-only Radio.Card presets. Every --noctis-radius-* step derives from it, so one preset re-rounds every rounded-* corner.
  • --noctis-seed-density — the Density picker (default 1), the same Radio.Card preset model. Every spacing step resolves through calc(<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.

StepDerivationAt seed 9999pxAt seed 0
xsmin(seed × 0.5, 0.375rem)0.375rem (capped)0
smmin(seed × 0.75, 0.5rem)0.5rem (capped)0
mdmin(seed, 0.625rem)0.625rem (capped)0
lgmin(seed × 1.5, 0.875rem)0.875rem (capped)0
xlmin(seed × 2, 1.25rem)1.25rem (capped)0
controlvar(--noctis-seed-radius) (uncapped)9999px (full round)0 (square)
full9999px (constant)9999px9999px

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 → statecssVarName reads token.category for the canonical segment, so the spelling falls out of the data rather than a hand-typed string:

The naming grammarEach segment is a field on the token object, joined in this order. Omitted fields drop out cleanly — a role has no component, a root part no anatomy, the rest state no suffix.
menu
componentomitted for roles & scales
item
anatomydata-slot minus the prefix
background-color
propertya real CSS property
hover
stateclosed set; rest omitted
Each segment is a field on the token object, joined in this order. Omitted fields drop out cleanly — a role has no component, a root part no anatomy, the rest state no suffix.

The component segment is omitted for roles and scales. The anatomy segment is the part's data-slot name minus the component prefix (menu-itemitem), 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:

CSS
/* 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.

Token viewerEvery token, searchable, with live values and where it's used.
Token graphFollow any token through the alias graph; pick a component to see everything it touches.
Open