Token Architecture

How nnuikit's 3-layer design token system makes theming, dark mode, and customization scalable — without hundreds of overrides.

The 3 Layers

nnuikit's component system is built on three layers of CSS custom properties. Each layer has a single responsibility — and together they let you switch themes, enable dark mode, or override one component, all with minimal CSS.

1

What colors exist?

Base Palette

Raw named color values — just shades of each hue, no semantic meaning attached. Changing these 12 values rebrands your entire UI.

/* All brand shades live here */
--color-brand-25:  #f5f3ff;
--color-brand-100: #ede9fe;
--color-brand-400: #a78bfa;
--color-brand-700: #6d28d9;
--color-brand-800: #5b21b6;
--color-brand-950: #1e0a4a;
2

What do they mean?

Semantic Tokens

Maps raw shades to roles. This is where intent lives — "primary button surface", "subtle badge background", "disabled state". Changing this layer enables dark mode, because dark mode remaps which shade fills each role.

/* Roles — point at base palette shades */
--color-surface-brand-primary:    var(--color-brand-800);
--color-surface-brand-secondary:  var(--color-brand-700);
--color-surface-brand-quaternary: var(--color-brand-400);  /* disabled */
--color-surface-brand-l1:         var(--color-brand-25);   /* subtle tint */
3

Where exactly?

Component Tokens

Maps semantic roles to specific component states. Each component reads only its own tokens, which point at semantic tokens. This isolates components — you can override one button variant without affecting anything else.

/* Component reads these — they point up the chain */
--color-button-brand-default-surface:  var(--color-surface-brand-primary);
--color-button-brand-hover-surface:    var(--color-surface-brand-secondary);
--color-button-brand-disabled-surface: var(--color-surface-brand-quaternary);

Why Not Just Use --color-brand-800 Directly?

If you hardcode var(--color-brand-800) in a component, inverting all shades for dark mode breaks some use cases while fixing others — because different parts of the UI remap in opposite directions.

Use CaseLight ModeDark ModeDirection
Button bg (prominent)brand-800brand-200dark → light
Button hoverbrand-700brand-300dark → light
Badge bg (subtle)brand-100brand-900light → dark
Borderbrand-200brand-800light → dark
Disabled statebrand-400brand-600middle shift

Button surfaces flip from dark to light. Badge backgrounds flip from light to dark. Disabled states shift toward the middle. There's no single inversion formula — each role has its own mapping. That's exactly what Layer 2 encodes.

Choosing the Right Layer to Override

ScenarioOverride LayerLines of CSS
Rebrand to a new colorLayer 1 — base palette~12
Enable dark modeLayer 2 — semantic tokens~30
Switch to a preset theme (blue → violet)Layer 1 — swap brand palette~12
One component exceptionLayer 3 — component token1

Without this system: 5 themes × 2 modes × 50 components × ~8 properties = thousands of overrides. With 3 layers: roughly 100 total.

One Change, Everything Updates

Here's what happens when you switch to the violet theme. A single Layer 1 override cascades through all three layers automatically:

/* 1. Layer 1: swap the base palette (12 lines) */
.violet {
  --color-brand-700: var(--color-violet-700);
  --color-brand-800: var(--color-violet-800);
  /* ... all 12 shades remapped */
}

/* 2. Layer 2: auto-updates — no changes needed */
--color-surface-brand-primary: var(--color-brand-800);
/*                              ^ now resolves to violet-800 */

/* 3. Layer 3: auto-updates — no changes needed */
--color-button-brand-default-surface: var(--color-surface-brand-primary);
/*                                     ^ now resolves to violet-800 */

12 lines of CSS. Every component, every state, every mode — updated instantly.

Token Categories

Color Tokens

Surface, text, icon, and border colors for all component states.

Spacing Tokens

Padding, gap, and margin values for each component size (sm/md/lg).

Size Tokens

Heights, widths, icon sizes, and border radius by size variant.

Theme Variants

CSS classes (.blue, .violet, .pink, .cyan, .orange) that swap Layer 1.

Adding Tokens to Your Project

When you run npx nnuikit init, the base design tokens are written to src/routes/layout.css and imported automatically. Component tokens are added when you run npx nnuikit add <component>.

/* layout.css — after running init and adding button */
@import 'tailwindcss';
/* base tokens are injected directly into layout.css by: npx nnuikit init */
@import '$lib/components/ui/button/tokens.css';           /* button component tokens */

Customizing at the Right Layer

/* Change brand color globally — override Layer 1 */
:root {
  --color-brand-700: #7c3aed;
  --color-brand-800: #6d28d9;
}

/* Enable dark mode — override Layer 2 */
.dark {
  --color-surface-brand-primary:   var(--color-brand-200);
  --color-surface-brand-secondary: var(--color-brand-300);
}

/* Override one component in one section — override Layer 3 */
.marketing-hero {
  --color-button-brand-default-surface: var(--color-surface-brand-tertiary);
}

TL;DR

1

Layer 1 — Base Palette

Raw color values. Override here to rebrand or switch themes.

2

Layer 2 — Semantic Tokens

Role mappings. Override here for dark mode — because modes remap which shade fills each role.

3

Layer 3 — Component Tokens

Component state mappings. Override here to break the pattern for one specific component.


The cost is a bit more setup once. The benefit is dramatically less work as the system grows.