Token Hierarchy
NDS tokens are layered in four tiers. Always reference the highest tier that carries the meaning you need: a component token before a semantic one, a semantic token before a raw value. Higher tiers resolve down to lower ones, so a single re-theme or dark-mode switch cascades everywhere. To retheme the whole system from a few seed colors, see Themes.
The raw --colors-* values: brand slots, neutrals, status hues, and alpha ramps. Names carry no meaning, only shade. Never edited directly: re-theming regenerates the brand slots from seed colors.
The dimension vocabulary: named spacing, radius, and typography sizes such as --spacing-xl and --radius-md, each carrying its value directly. The size names are the entire scale.
One name per meaning, system-wide: backgrounds, text, borders, icons, form-control surfaces, and shadows. Dark mode and re-themes re-bind this layer, so everything that references it adapts automatically.
Per-component dials named --{component}-{property}-{variant}-{state}. Override one to retune a single component everywhere without touching the shared meanings other components consume.
Naming Grammar
Token names are contracts: they state what a value is for, never what color or shade it happens to be. Every name follows one of two patterns, built from a fixed vocabulary. The one sanctioned number is the opacity step in alpha families (--colors-green-alpha-10 is a 10% wash), matching the palette's own alpha convention.
Patterns
| Tier | Pattern | Example |
|---|---|---|
| Semantic | --{property}-{role}-{modifier}-{state} | --text-oncolor-primary, --background-error-light |
| Component | --{component}-{property}-{variant}-{state} | --button-background-primary-hovered |
Vocabulary
| Term | Meaning |
|---|---|
light | A tinted wash of the base meaning, for soft surfaces and borders |
faint | The faintest tint, one step below light: faint < light < solid |
strong | A deep, emphasized variant of the base meaning |
oncolor | The element sits on a colored or dark fill, so the value stays legible there in every mode. Always placed last before the state |
default / hovered / pressed / selected / focused / disabled / checked | The state axis. A name without a state suffix is the resting state; pressed corresponds to :active |
Primitive tokens
The raw value layer: the DGA color palette and the dimension scales. Each named size (--spacing-xl, --radius-md) carries its value directly; the size names are the whole scale.
Semantic tokens
The meaning layer. Each token indirects through a primitive, so overriding the palette or switching to dark mode re-resolves every consumer with no component changes.
Component tokens
Per-component tokens, grouped by component. Each consumes a semantic or palette token; override one on a scope to restyle just that component.
Knobs vs Tokens
NDS has two override surfaces with different jobs. Tokens theme the system: defined at :root, changing one retunes every matching component. Knobs style one element: undefined by default, set inline or on a wrapper, scoped to whatever they cascade into.
| Tokens | Knobs | |
|---|---|---|
| Job | Theme the system or a whole component type | Style one instance or one region |
| Default | Defined at :root by NDS | Undefined; the component falls back internally |
| Where you set it | :root or a theme scope | The element's style attribute or a wrapper class |
| Examples | --button-background-primary-default, --background-card | --btn-size, --card-width, --section-padding-block |
/* Knob: this one button renders at 48px */
<button class="nds-btn nds-primary" style="--btn-size: 48px">Save</button>
/* Token: every primary button in the app changes fill */
:root { --button-background-primary-default: var(--colors-tertiary-600); }
Each component resolves its knobs through a private variable with a fallback, so an unset knob simply uses the component's default. Every component page lists its knobs in the CSS Custom Properties table of its Usage Guidelines.
Dark Mode Behavior
Dark mode activates with <html data-theme="dark"> (token-matched, so it composes with theme names like "dark crimson"). It changes no component CSS: it only re-binds tokens.
- The semantic layer flips first: page surfaces, text, borders, icons, and shadows take dark values, and everything consuming them follows
- Component dials re-bind where a component needs its own dark correction: checkbox and switch fills darken for white-glyph contrast while radio brightens for its dot-with-border shape
- The oncolor family deliberately does not flip: elements on brand fills keep their contrast in both modes
- The palette never changes:
--colors-neutral-100is the same hex in both modes, which is why components reference meanings, not shades
Overriding with dark in mind
/* One value for both modes: override the token once */
:root { --background-card: #fffdf5; }
/* A dark-specific value: re-bind it inside the dark scope */
:root[data-theme~="dark"] { --background-card: #26221a; }
Mode switching, persistence, and the seed-color theme engine are covered on the Themes page.
Usage Guidelines
Best Practices
- Reference tokens with
var(--token-name). Never hardcode a hex color, pixel value, or shadow that a token already names - Prefer the highest tier that fits: a component token over a semantic token over a raw primitive. The higher the tier, the more a re-theme or dark mode does for you for free
- Reach for a primitive directly only when no semantic token carries the meaning (for example a one-off spacing or radius value)
- To restyle a single component, override its
--{component}-*token on a scope rather than editing the component's SCSS:.my-scope { --button-background-primary-default: var(--colors-tertiary-600); } - To retheme the whole system, override the brand slots (
--colors-primary-*and friends). Semantic and component tokens re-resolve automatically. The Themes page generates a full palette from a few seed colors - Do not hardcode light-mode values. Dark mode re-binds the semantic layer, so a component that references
--text-defaultflips correctly while one pinned to--colors-base-blackwill not - Always pair a typography size token with its line-height:
--typo-text-md-FSwith--typo-text-md-LH - Match spacing by value when reading the scale:
--spacing-xlis 16px,--spacing-mdis 8px - Use knobs (
--btn-size,--card-width) for one-off instance styling and tokens for theme-wide changes; a knob set on a wrapper styles everything inside it - When you override a component token globally, give it a dark value too by re-binding it inside
:root[data-theme~="dark"]
How Tokens Resolve
Every painted value walks a chain from a knob or component dial down to the palette. Two real chains from the source:
/* The .nds-primary button fill: knob, component dial, palette */
--btn-bg: var(--button-background-primary-default); /* knob, set by the variant class */
--button-background-primary-default: var(--colors-primary-600); /* component tier */
--colors-primary-600: #1b8354; /* palette */
/* The checked checkbox fill: dial, shared semantic surface, palette */
--checkbox-primary-checked: var(--controls-primary-checked); /* component dial */
--controls-primary-checked: var(--colors-primary-600); /* semantic */
Override any link in the chain and everything above it follows. This reference is generated directly from the token SCSS at build time, so the names and values on this page always match what ships.