Multi Namespace Token Strategy
The Problem
A website starts small — a blog with article pages. The design team creates tokens under a single namespace:
@theme {
/* myweb- namespace: article-focused design */
--spacing-myweb-hsp-sm: 20px;
--spacing-myweb-hsp-md: 40px;
--spacing-myweb-vsp-sm: 24px;
--spacing-myweb-vsp-md: 48px;
--font-size-myweb-body: 1.125rem;
--font-size-myweb-h1: 2.5rem;
--font-size-myweb-h2: 1.75rem;
--font-size-myweb-caption: 0.875rem;
}This works. Spacing is generous for readability. Font sizes are optimized for long-form content. Every component shares the same design language.
Then the site grows. A login page arrives. Then a user settings panel. Then an admin dashboard with data tables, dense toolbars, and compact navigation. The admin UI needs tighter spacing, smaller font sizes, and a denser layout — the opposite of what the article tokens provide.
The instinct is to add more tokens to the same namespace:
@theme {
/* Original article tokens */
--spacing-myweb-hsp-sm: 20px;
--spacing-myweb-hsp-md: 40px;
--spacing-myweb-vsp-sm: 24px;
--spacing-myweb-vsp-md: 48px;
/* ...now add admin-density tokens into the same namespace */
--spacing-myweb-hsp-dense-sm: 8px;
--spacing-myweb-hsp-dense-md: 16px;
--spacing-myweb-vsp-dense-sm: 6px;
--spacing-myweb-vsp-dense-md: 12px;
--font-size-myweb-body: 1.125rem;
--font-size-myweb-body-dense: 0.8125rem;
--font-size-myweb-h1: 2.5rem;
--font-size-myweb-h1-dense: 1.25rem;
--font-size-myweb-h2: 1.75rem;
--font-size-myweb-h2-dense: 1rem;
--font-size-myweb-caption: 0.875rem;
--font-size-myweb-caption-dense: 0.75rem;
}The token set doubles in size. Every axis now has both "article" and "dense" variants. Developers must choose between hsp-sm and hsp-dense-sm for every spacing decision. The tight token strategy — designed to constrain choices — now offers twice as many options as before.
As the site adds more contexts (marketing landing pages, email templates, embedded widgets), the problem compounds. A single namespace tries to serve every design context, and the token set becomes a bloated list of variations.
The Solution
Split tokens into separate namespaces, one per design context. Each namespace contains a focused, minimal token set that serves exactly one type of UI.
/* ── Article/content context ── */
@theme {
--spacing-myweb-hsp-sm: 20px;
--spacing-myweb-hsp-md: 40px;
--spacing-myweb-vsp-sm: 24px;
--spacing-myweb-vsp-md: 48px;
--font-size-myweb-body: 1.125rem;
--font-size-myweb-h1: 2.5rem;
--font-size-myweb-h2: 1.75rem;
--font-size-myweb-caption: 0.875rem;
}
/* ── Admin/dashboard context ── */
@theme {
--spacing-myadmin-hsp-sm: 8px;
--spacing-myadmin-hsp-md: 16px;
--spacing-myadmin-vsp-sm: 6px;
--spacing-myadmin-vsp-md: 12px;
--font-size-myadmin-body: 0.8125rem;
--font-size-myadmin-h1: 1.25rem;
--font-size-myadmin-h2: 1rem;
--font-size-myadmin-caption: 0.75rem;
}The total token count is the same, but the structure is different:
Each namespace has exactly 4 spacing + 4 font-size tokens — the same tight constraint as a single-context project
Developers working on article pages only interact with
myweb-tokensDevelopers working on the admin dashboard only interact with
myadmin-tokensThere is no ambiguity about which token to use — the namespace tells you the context
File Organization
Separate namespaces work best when each lives in its own CSS file:
styles/
├── tokens-myweb.css /* Article/content tokens */
├── tokens-myadmin.css /* Admin/dashboard tokens */
├── tokens-shared.css /* Color, font-family, breakpoints — shared */
└── app.css /* Imports all token files */ Shared tokens (colors, font families, breakpoints) that apply across all contexts stay in a shared file. Only context-specific tokens (spacing, font sizes) are split into namespaces.
/* app.css */
@import "tailwindcss/preflight";
@import "tailwindcss/utilities";
@import "./tokens-shared.css";
@import "./tokens-myweb.css";
@import "./tokens-myadmin.css";Demos
Visual Contrast: Article vs Admin UI
The same page framework rendered with two different token namespaces. Article tokens produce generous, readable spacing. Admin tokens produce a dense, efficient layout.
When to Split: Decision Boundary
Not every page variation needs its own namespace. The split makes sense when design rules — spacing rhythm, font size scale, density — are fundamentally different between contexts.
Namespace Usage in Components
Each namespace produces its own set of Tailwind utility classes. Developers working in a specific context use only that context's namespace prefix.
Shared vs Context-Specific Tokens
Not all token categories need namespace separation. The rule: split tokens that define density and scale (spacing, font sizes). Keep tokens that define identity (colors, font families, border radii) shared.
| Token category | Split or shared? | Why |
|---|---|---|
| Spacing (hsp, vsp) | Split per context | Different contexts need different density |
| Font sizes | Split per context | Article text vs dashboard text have different scales |
| Colors | Shared | Brand identity stays consistent across contexts |
| Font families | Shared | Typography choice is a brand decision |
| Border radii | Shared | Visual style is consistent across contexts |
| Breakpoints | Shared | Responsive behavior follows the same viewport rules |
light-dark() scheme switching | Per-namespace decision | Screen-only chrome defaults to it; print-first namespaces skip it |
/* tokens-shared.css — applies everywhere */
@theme {
--color-brand: oklch(55.5% 0.163 48.998);
--color-surface: hsl(0 0% 100%);
--color-text: hsl(222 47% 11%);
--color-muted: hsl(215 16% 47%);
--color-border: hsl(214 32% 91%);
--font-sans: system-ui, sans-serif;
--font-mono: ui-monospace, monospace;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
}Print-First Namespaces Skip light-dark()
One color decision stays per-namespace even when the palette itself is shared: whether tokens go through light-dark(). An app-chrome namespace renders on screens only, so its palette defaults to light-dark() — one token serves both schemes. A print-first namespace — slides, invoices, certificates — must render identically on screen and on paper. Print pipelines force a light color-scheme, so a light-dark() token resolves to its light value on paper while the on-screen preview may show the dark value — the preview and the printed page diverge. Define a print-first palette as plain values with no light-dark() and no scheme switching.
/* ── App-chrome namespace: screen-only, scheme-aware ── */
:root {
color-scheme: light dark; /* required — without it light-dark() always picks the first value */
}
@theme {
--color-mychrome-surface: light-dark(hsl(0 0% 100%), hsl(222 20% 12%));
--color-mychrome-text: light-dark(hsl(222 47% 11%), hsl(210 20% 88%));
}
/* ── Print-first namespace: identical on screen and paper ── */
@theme {
--color-myslides-surface: hsl(0 0% 100%);
--color-myslides-text: hsl(222 47% 11%);
--color-myslides-accent: hsl(30 80% 45%);
}The Tier-Order Naming Trap
A namespace often uses a two-tier token structure: a raw palette tier that holds the actual values, and a semantic tier that maps them to meaning (see Design Token Lint for the enforcement side). The two tiers spell the namespace in opposite orders, and the reversal is easy to miss.
The raw tier is prefix-first. It consists of plain custom properties outside @theme — they generate no utilities, so the namespace leads to prevent collisions:
/* Raw tier — prefix-first: --myns-palette-* */
:root {
--myns-palette-accent-4: hsl(30 80% 50%);
--myns-palette-neutral-2: hsl(215 15% 88%);
}The framework-facing semantic tier is category-first, because Tailwind keys utility generation on the category prefix — --color-* produces bg-* and text-* utilities:
/* Semantic tier — category-first: --color-myns-* */
@theme {
--color-myns-accent: var(--myns-palette-accent-4);
--color-myns-border: var(--myns-palette-neutral-2);
}myns leads in the raw tier and follows the category in the semantic tier. Code written from memory tends to invent a third spelling that matches neither:
/* NG — a third spelling: connects to neither tier */
.myns-chrome__panel {
border-color: var(--myns-color-accent); /* nothing ever defines this */
}A third spelling means a disconnected namespace. A real review found chrome CSS consuming --myns-color-accent while the semantic tier defined --color-myns-accent — every panel-driven token tweak was silently dead because the chrome read variables nothing ever wrote. CSS tolerates undefined custom properties, so nothing errors; the styles just stop responding to the token layer.
Grep for the wrong-order spelling as a drift check:
# Drift check — the reversed spelling must return zero hits
grep -rn -- "--myns-color-" src/ # 0 hits expected
grep -rn -- "--color-myns-" src/ # semantic tier (legitimate)
grep -rn -- "--myns-palette-" src/ # raw tier (legitimate)Quick Reference
| Scenario | Approach |
|---|---|
| Single design context (blog, marketing site) | One namespace — no split needed |
| Two distinct UI densities (content + admin) | Two namespaces, one per context |
| Three or more contexts (content + admin + embedded widget) | One namespace per context, shared tokens in a common file |
| Colors, font families, border radii | Keep in shared namespace — these define identity, not density |
| Spacing and font sizes | Split per context — these define density and scale |
| Team ownership boundaries (frontend team vs admin team) | Align namespaces with team boundaries for clear ownership |
| Print-destined context (slides, invoices, certificates) | Own namespace with plain color values — no light-dark() |
| Two-tier tokens (raw palette + semantic) | Raw tier is prefix-first (--myns-palette-*), semantic tier is category-first (--color-myns-*) |
| Namespace bring-up verification | Grep the built CSS — utilities emitted, tokens consumed |
Common AI Mistakes
Mixing tokens from different namespaces in one component — if a component uses
px-myweb-hsp-smfor horizontal padding andtext-myadmin-bodyfor font size, it is pulling from two design contexts; each component should use tokens from exactly one namespaceCreating a namespace for every page — namespaces represent design contexts (content vs admin), not individual pages; a blog post and an about page share the same design context
Splitting colors into namespaces — colors define brand identity and should remain shared; only density-related tokens (spacing, font sizes) need namespace separation
Using generic token names without a namespace prefix — in a multi-namespace project,
hsp-smis ambiguous; always use the full prefix (myweb-hsp-smormyadmin-hsp-sm)Creating namespaces preemptively — start with one namespace; split only when a genuinely different design context arrives with different density requirements
Inventing a third namespace spelling — the raw tier is prefix-first (
--myns-palette-accent-4) and the semantic tier is category-first (--color-myns-accent); a from-memory--myns-color-accentconnects to neither tier and every reference to it is silently deadVerifying token wiring from source alone — a utility whose token the theme never defined emits zero CSS with no error; grep the built stylesheet to confirm the utility and its token actually appear
When to Use
Good fit
Sites with distinct UI contexts — a content-heavy site that adds an admin dashboard, or a marketing site that adds an application UI
Large teams with separate ownership — when the article team and the admin team work independently, separate namespaces prevent cross-contamination of design decisions
Projects using the tight token strategy — namespace separation is a natural extension when the single namespace accumulates too many variants
Not needed
Single-context websites — a blog, a documentation site, or a portfolio does not need multiple namespaces
Small projects — if the token set is small and manageable, the complexity of multiple namespaces adds overhead without benefit
Projects early in development — start with one namespace and split later when a second design context actually arrives
Contrast with Other Token Strategies
This strategy builds on top of the tight token strategy. It does not replace it — it extends it for multi-context projects.
| Strategy | Scope | When |
|---|---|---|
| Tight Token Strategy | Single namespace, constrained tokens | Default for all projects |
| Two-Tier Size Strategy | Theme tokens + arbitrary values for width/height | When sizing elements within any namespace |
| Multi Namespace Token Strategy | Multiple namespaces for different UI contexts | When a project serves fundamentally different design contexts |
Build-time enforcement
A multi-namespace token system needs the same enforcement boundary as a single-namespace one — arguably more, because each new namespace expands the surface where raw values can hide. See Design Token Lint for the multi-pass linter pattern that catches raw literals in component code and validates that semantic tokens reference palette tokens via var().
Audit the Built CSS, Not the Source
Two failure modes are invisible in source review and only appear in the emitted stylesheet. First, a utility class can silently no-op: with a tight theme, tracking-widest in markup emits zero CSS when the theme does not define --tracking-widest. The class sits in the markup looking intentional, Tailwind generates nothing, and no error is raised. Second, semantic tokens can be defined but never consumed — the disconnected-namespace case above, where components reference a third spelling and the real tokens feed nothing.
Make a post-build grep part of namespace bring-up:
# 1. Every utility used in markup must appear in the built CSS
grep -o "tracking-widest" dist/assets/*.css | wc -l
# 0 = the class no-ops: the theme lacks --tracking-widest,
# so Tailwind generated no rule for it
# 2. Every semantic token must be defined AND consumed
grep -o -- "--color-myns-accent" dist/assets/*.css | wc -l
# 1 = defined once, referenced nowhere — dead or disconnected
# (grep -o counts occurrences — minified CSS puts everything on one line)Token lint catches raw literals in component code — it does not catch a utility whose token the theme never defined. Nothing in that markup is "raw"; the utility is simply undefined, and only the built output shows it. The two checks cover different failure classes and do not replace each other.