Siddhant Deval
Siddhant DevalAuthor
Senior Full-Stack Engineer·Sep 22, 2026·15 min read

Design System Architecture: Token Engines, Headless Primitives & the 3-Layer Model

A design system has exactly two independently evolvable layers — design tokens and headless primitives. This article builds the W3C DTCG token pipeline, explains why headless accessible components own logic but not visual presentation, and maps the 3-layer architecture that eliminates upgrade paralysis.

Design System Architecture: Token Engines, Headless Primitives & the 3-Layer Model

Architecture is not about drawing boxes on a whiteboard — it is about enforcing boundary contracts, deterministic caching, and secure data mediation across independent release units. Design systems fail not because they lack components, but because they fail to enforce the right boundaries between layers. When the layer that holds color values is bundled with the layer that holds button implementations, a brand refresh requires a major version bump, and twelve teams halt their work to test that their buttons still render.
This article establishes the architecture that eliminates upgrade paralysis: a strict three-layer separation between design tokens, headless accessible primitives, and composite domain components. The boundary between these layers is not a convention — it is a structural constraint enforced by your publishing strategy.

1. The Two Failure Modes of Design Systems

Most design systems fail in one of two ways. Understanding both is necessary before designing anything.

1.1 The Monolithic Library Trap

The monolithic design system ships as a single npm package: @company/design-system. It exports everything — color tokens, spacing values, Button, Input, Modal, DataTable, GlobalNavigation, CheckoutSummary. Every consuming app imports from one place.
This feels like convenience. It is a coupling bomb.
typescript
// ❌ Consuming a monolithic design system
import { Button, GlobalNav, CheckoutSummary } from '@company/design-system'

// When the design team updates the brand color palette,
// @company/design-system publishes v3.0.0.
//
// v3.0.0 also includes:
//   - Button API change: 'variant' prop renamed to 'intent'
//   - GlobalNav: added required 'user' prop
//   - CheckoutSummary: requires new cart context
//
// To get the color update, you must adopt all breaking changes.
// Your team stops feature work for two weeks.
The root cause: tokens, primitives, and domain components are tightly coupled. They version together, they break together, and they require coordinated upgrades across every consuming team simultaneously.

1.2 The Unstyled Chaos Trap

The opposite failure: no shared design system at all. Each team builds their own buttons, their own modals, their own navigation. Every team reinvents the same accessibility patterns, every team makes different typographic decisions, and the product looks like five separate companies.
The correct architecture is neither. It is a structured separation of concerns across three independently versioned layers.

2. The 3-Layer Design System Model

Layer 3: Composite Domain Components
  └── (owned per product area, never shared cross-domain)
      Examples: CheckoutSummary, UserProfileCard, ProductListingRow

Layer 2: Headless Accessible Primitives
  └── (shared, unstyled, stable — logic + a11y, no visual opinions)
      Examples: Dialog, Combobox, Tooltip, RadioGroup, DatePicker

Layer 1: Token Engine
  └── (shared, zero-runtime, versioned frequently — pure data)
      Examples: color.brand.primary, spacing.md, typography.heading.xl
Each layer has a different release cadence, a different owner, and a different distribution strategy. They are not sub-packages of one library — they are separate publishing units.
LayerOwnerRelease CadenceDistributionRuntime Impact
1 — Token EngineDesign Systems TeamHigh (brand updates)Versioned NPM packageZero (CSS variables, build-time constants)
2 — Headless PrimitivesDesign Systems TeamLow (API-stable)Versioned NPM packageMinimal (component logic only, no styles)
3 — Domain ComponentsProduct Team (per domain)IndependentNo sharing — owned locallyContained (per-app bundle)

3. Layer 1 — The W3C Design Tokens Community Group (DTCG) Specification

3.1 Why Ad-Hoc Token Schemas Create Migration Debt

For years, design teams used proprietary JSON schemas to define tokens:
json
// ❌ Ad-hoc proprietary token format — incompatible with all tooling
{
  "colors": {
    "brand": {
      "primary": "#0070f3",
      "secondary": "#ff6b6b"
    }
  },
  "spacing": {
    "sm": "8px",
    "md": "16px"
  }
}
Every tool (Style Dictionary, Token Studio, Theo, Chromatic) invented its own transformation pipeline for this format. The schemas are incompatible. Migrating between tools means rewriting every token file.

3.2 The DTCG Standard ($type, $value)

The W3C Design Tokens Community Group specification defines a portable interchange format now adopted by Style Dictionary 4.x, Token Studio, Figma Variables, and most major design toolchains:
json
// ✅ W3C DTCG format — portable across all DTCG-compatible tooling
{
  "color": {
    "brand": {
      "primary": {
        "$type": "color",
        "$value": "#0070f3",
        "$description": "Primary action color — used for CTAs, links, and focus rings"
      },
      "secondary": {
        "$type": "color",
        "$value": "#ff6b6b"
      }
    }
  },
  "spacing": {
    "sm": { "$type": "dimension", "$value": "8px" },
    "md": { "$type": "dimension", "$value": "16px" },
    "lg": { "$type": "dimension", "$value": "32px" }
  },
  "typography": {
    "heading": {
      "xl": {
        "$type": "typography",
        "$value": {
          "fontFamily": "Inter, system-ui, sans-serif",
          "fontSize": "36px",
          "fontWeight": 700,
          "lineHeight": 1.2
        }
      }
    }
  }
}
DTCG types include: color, dimension, fontFamily, fontWeight, duration, cubicBezier, number, strokeStyle, border, typography, shadow, gradient, transition.

3.3 Token Aliasing and Semantic Tokens

Tokens can reference other tokens through aliasing — creating a two-tier system of primitive tokens (raw values) and semantic tokens (contextual meaning):
json
{
  "primitive": {
    "blue": {
      "500": { "$type": "color", "$value": "#0070f3" },
      "700": { "$type": "color", "$value": "#0051a8" }
    }
  },
  "semantic": {
    "color": {
      "action": {
        "default": {
          "$type": "color",
          "$value": "{primitive.blue.500}"
        },
        "hover": {
          "$type": "color",
          "$value": "{primitive.blue.700}"
        }
      }
    }
  }
}
Semantic tokens decouple the meaning of a color (action.default) from its raw value (blue.500). A brand refresh that changes the primary blue from #0070f3 to #0062d1 requires updating one primitive token — all semantic tokens that reference it update automatically.
Pro Tip & Optimization
Always build two token tiers: primitive (exact values, named after what they are) and semantic (named after where they are used, aliasing primitives). Never allow consuming components to reference primitive tokens directly — only semantic tokens. This is what makes brand evolution non-breaking.

4. Layer 1 — The Style Dictionary 4.x Pipeline

Style Dictionary transforms DTCG JSON source tokens into platform-specific outputs: CSS custom properties, TypeScript constants, iOS Swift Color extensions, Android XML resources.

4.1 Configuration

javascript
// style-dictionary.config.mjs
import StyleDictionary from 'style-dictionary'

const sd = new StyleDictionary({
  source: ['tokens/**/*.json'],
  platforms: {
    css: {
      transformGroup: 'css',
      prefix: 'ds',
      buildPath: 'dist/css/',
      files: [
        {
          destination: 'tokens.css',
          format: 'css/variables',
          options: {
            outputReferences: true  // Emit var() references, not resolved values
          }
        }
      ]
    },
    js: {
      transformGroup: 'js',
      buildPath: 'dist/js/',
      files: [
        {
          destination: 'tokens.js',
          format: 'javascript/es6'
        },
        {
          destination: 'tokens.d.ts',
          format: 'typescript/es6-declarations'
        }
      ]
    }
  }
})

await sd.buildAllPlatforms()

4.2 Generated Outputs

CSS custom properties (dist/css/tokens.css):
css
:root {
  --ds-color-brand-primary: #0070f3;
  --ds-color-action-default: var(--ds-color-brand-primary);
  --ds-color-action-hover: #0051a8;
  --ds-spacing-sm: 8px;
  --ds-spacing-md: 16px;
  --ds-typography-heading-xl-font-size: 36px;
  --ds-typography-heading-xl-font-weight: 700;
}
TypeScript constants (dist/js/tokens.d.ts):
typescript
export declare const ColorBrandPrimary: string
export declare const ColorActionDefault: string
export declare const SpacingSm: string
export declare const TypographyHeadingXlFontSize: string

4.3 Consuming Tokens in Application Code

css
/* In a component's CSS Module */
.button {
  background-color: var(--ds-color-action-default);
  padding: var(--ds-spacing-sm) var(--ds-spacing-md);
}

.button:hover {
  background-color: var(--ds-color-action-hover);
}
typescript
// In a component using TypeScript constants (for non-CSS contexts, e.g., charting, Canvas)
import { ColorBrandPrimary } from '@company/tokens/js'

const chart = new Chart(ctx, {
  options: {
    plugins: {
      legend: { labels: { color: ColorBrandPrimary } }
    }
  }
})
Crucial Requirement
CSS custom properties are the preferred token delivery mechanism for web applications. They are resolved at runtime by the browser, enabling theming through :root overrides and media query variations. TypeScript constants are for contexts where CSS variables cannot be used (Canvas APIs, third-party charting libraries, email templates).

5. Layer 2 — Headless Accessible Primitives

5.1 Why Headless Primitives Exist

Building an accessible Dialog, Combobox, or DatePicker from scratch requires implementing:
  • ARIA roles, attributes, and live regions.
  • Keyboard navigation (focus trapping, arrow key traversal, Escape to close).
  • Screen reader announcements.
  • RTL (right-to-left) layout support.
  • Mobile touch event handling.
This is weeks of work per component, per team. The answer is to share the accessibility logic while keeping the visual presentation entirely local to each team.

5.2 Headless Libraries in 2026

LibraryMaintained ByApproachBest For
Radix UIWorkOSUnstyled React primitives with full a11yReact apps needing Tailwind or CSS Modules styling
React AriaAdobeHook-based, platform-agnostic, WCAG 2.2Strict accessibility requirements
Base UIMUIHeadless React, composable slotsTeams migrating from MUI
AriakitDiego HazHook-based, flexible compositionCustom design systems

5.3 Usage Pattern — Radix UI Dialog with Custom Styling

typescript
// packages/ui/src/Dialog/Dialog.tsx
import * as RadixDialog from '@radix-ui/react-dialog'
import styles from './Dialog.module.css'

interface DialogProps {
  trigger: React.ReactNode
  title: string
  description?: string
  children: React.ReactNode
  onOpenChange?: (open: boolean) => void
}

export function Dialog({ trigger, title, description, children, onOpenChange }: DialogProps) {
  return (
    <RadixDialog.Root onOpenChange={onOpenChange}>
      <RadixDialog.Trigger asChild>
        {trigger}
      </RadixDialog.Trigger>
      <RadixDialog.Portal>
        <RadixDialog.Overlay className={styles.overlay} />
        <RadixDialog.Content className={styles.content}>
          <RadixDialog.Title className={styles.title}>
            {title}
          </RadixDialog.Title>
          {description && (
            <RadixDialog.Description className={styles.description}>
              {description}
            </RadixDialog.Description>
          )}
          {children}
          <RadixDialog.Close asChild>
            <button className={styles.closeButton} aria-label="Close dialog"></button>
          </RadixDialog.Close>
        </RadixDialog.Content>
      </RadixDialog.Portal>
    </RadixDialog.Root>
  )
}
css
/* packages/ui/src/Dialog/Dialog.module.css */
/* All values reference Layer 1 tokens — no hardcoded values */
.overlay {
  position: fixed;
  inset: 0;
  background-color: rgb(0 0 0 / 50%);
}

.content {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: var(--ds-color-surface-elevated);
  border-radius: var(--ds-radius-lg);
  padding: var(--ds-spacing-lg);
  max-width: 480px;
  width: 90vw;
}

.title {
  font-size: var(--ds-typography-heading-sm-font-size);
  font-weight: var(--ds-typography-heading-sm-font-weight);
  color: var(--ds-color-text-primary);
}
Radix handles all ARIA attributes, focus trapping, keyboard dismissal, and portal rendering. The team handles visual presentation entirely through CSS that references Layer 1 tokens.
Architectural Note
The critical principle: packages/ui (Layer 2) imports from @company/tokens (Layer 1). It never imports from any app-level package. This is the boundary that @nx/enforce-module-boundaries enforces — UI libraries cannot depend on apps.

6. Layer 3 — Composite Domain Components

6.1 What Belongs in Layer 3

Layer 3 components encode business-domain concepts. They are not generic UI patterns — they are specific implementations for a product area:
typescript
// ❌ This should NOT be in packages/ui — it encodes checkout domain logic
export function CheckoutSummary({ cart, user, promoCode }: CheckoutSummaryProps) {
  const discount = usePromoCode(promoCode)
  const taxRate = useTaxRate(user.region)
  return (
    <div>
      {cart.items.map(item => <CartItem key={item.id} item={item} />)}
      <PriceSummary subtotal={cart.subtotal} discount={discount} tax={taxRate} />
    </div>
  )
}
CheckoutSummary uses usePromoCode and useTaxRate — hooks that contain business logic specific to the checkout domain. Placing this in packages/ui forces every team that uses the shared UI library to carry checkout business logic in their bundle.
typescript
// ✅ Layer 3 component lives in apps/checkout/src/components/
// It uses Layer 1 tokens and Layer 2 primitives, but is not shared.
import { Button } from '@company/ui'         // Layer 2 headless primitive
import { usePromoCode } from '../hooks'       // Local checkout domain hook

6.2 The Rule

Layer 3 components are never published as shared packages. They live in apps/checkout/src/ or packages/checkout-components/ with a scope tag that prevents any other domain from importing them. The design system team does not own Layer 3 — the product team does.
Top-down hierarchy diagram of the 3-layer design system architecture. Layer 1 (Token Engine) at the top: thin bar labeled 'W3C DTCG JSON → Style Dictionary → CSS custom properties + TS constants', colored cyan. Layer 2 (Headless Primitives) in the middle: labeled 'Radix UI / React Aria — accessibility logic, no visual opinions', colored text-white. Layer 3 (Domain Components) at the bottom: labeled 'CheckoutSummary, UserProfileCard — business logic, not shared', colored dim. Downward arrows show that Layer 2 consumes Layer 1, and Layer 3 consumes both.
Top-down hierarchy diagram of the 3-layer design system architecture. Layer 1 (Token Engine) at the top: thin bar labeled 'W3C DTCG JSON → Style Dictionary →…

7. CSS Custom Property Scoping & Shadow DOM Boundaries

7.1 The Inheritance Problem

CSS custom properties inherit through the DOM tree. A token defined on :root is available to all descendants. This is the mechanism that makes theming work.
The problem arises at Shadow DOM boundaries. Custom elements that use Shadow DOM (Web Components) do not inherit CSS custom properties from the document root by default — the shadow tree has its own cascade root.
css
/* ❌ This does NOT work across a Shadow DOM boundary */
:root {
  --ds-color-brand-primary: #0070f3;
}

/* Inside a Web Component's shadow root — this resolves to empty */
.button {
  background: var(--ds-color-brand-primary); /* ← inherits nothing */
}

7.2 The Fix: Explicit Token Forwarding

For components that render inside Shadow DOM, tokens must be forwarded explicitly:
css
/* ✅ Forward tokens into the shadow host */
my-button {
  --ds-color-brand-primary: #0070f3;
  --ds-spacing-sm: 8px;
}
Or in the component's shadow root stylesheet:
css
/* Web Component shadow stylesheet */
:host {
  --ds-color-brand-primary: var(--ds-color-brand-primary, #0070f3);
}
Crucial Requirement
Most React application teams do not use Shadow DOM — this is primarily a concern for Web Component-based micro-frontends or Angular Elements. If your design system targets React applications only, CSS custom properties work reliably without any forwarding configuration.
Left-to-right flow trace of the Style Dictionary 4.x pipeline: DTCG JSON token source files (color.$value, spacing.$value) → Style Dictionary 4.x transform pipeline → three output branches: CSS custom properties (--ds-color-brand-primary), TypeScript ES6 constants (ColorBrandPrimary: string), and platform outputs (iOS Swift / Android XML). Each branch labeled with its output file path.
Left-to-right flow trace of the Style Dictionary 4.x pipeline: DTCG JSON token source files (color.$value, spacing.$value) → Style Dictionary 4.x transform p…

Summary

ConceptRule
Token formatUse W3C DTCG ($type, $value) — never a proprietary JSON schema
Token tiersPrimitive tokens hold raw values; semantic tokens alias primitives and carry meaning
Headless primitivesOwn accessibility logic and keyboard behavior; never own visual presentation
Layer 3 ruleDomain components are never shared cross-domain — they live in the owning app or domain package
CSS custom propertiesThe correct web token delivery mechanism; use TypeScript constants only for non-CSS contexts
Shadow DOMCSS custom properties do not cross Shadow DOM boundaries without explicit forwarding

What's Next

In Part 4, we take the 3-layer architecture from this article and solve the hardest operational challenge: how to distribute tokens and components across independently deployed apps — evaluating static NPM packages, Module Federation runtime remotes, and the hybrid topology that is the production standard.
Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#Design System#Design Tokens#W3C DTCG#Style Dictionary#Radix UI#React Aria#Frontend Architecture
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.