← All articles

Design System Architecture 2026: Tokens, Components, and Governance That Lasts

Most design systems fail β€” abandoned, bloated, or inconsistent. This guide covers the architecture that works: Style Dictionary tokens synced to Figma, React component library with Radix UI, Figma-to-code sync, documentation, and governance model with ownership and versioning.

Design system architecture showing tokens, primitives, composites, patterns, and Figma-code sync pipeline

Design systems are the difference between a codebase that scales and one that becomes a graveyard of inconsistent components, duplicate CSS, and "quick fixes" that never get cleaned up.

This is the practical design system architecture we use at UI Designer β€” from tokens to documentation, with the exact file structure, tooling, and governance model that keeps it alive.

Why Most Design Systems Fail

| Failure Mode | Symptom | Root Cause | |--------------|---------|------------| | **Abandoned** | Figma file unused, code diverges | No ownership, no process | | **Bloated** | 50+ components, 10 used | Built for hypotheticals, not real needs | | **Inconsistent** | Button looks different everywhere | No single source of truth | | **Inaccessible** | Missing focus states, wcag-2026" class="internal-link">contrast failures | A11y bolted on, not baked in | | **Outdated** | Tokens in Figma β‰  tokens in code | No automation between design ↔ code |

**A living design system has three pillars: Tokens (source of truth), Components (implementation), Governance (process).**

---

Architecture Overview

design-system/
β”œβ”€β”€ tokens/                    # Design tokens (single source of truth)
β”‚   β”œβ”€β”€ src/                   # Token definitions (JSON/TS)
β”‚   β”‚   β”œβ”€β”€ color.json
β”‚   β”‚   β”œβ”€β”€ spacing.json
β”‚   β”‚   β”œβ”€β”€ typography.json
β”‚   β”‚   β”œβ”€β”€ border-radius.json
β”‚   β”‚   β”œβ”€β”€ shadows.json
β”‚   β”‚   β”œβ”€β”€ motion.json
β”‚   β”‚   β”œβ”€β”€ breakpoints.json
β”‚   β”‚   └── z-index.json
β”‚   β”œβ”€β”€ build/                 # Build scripts (Style Dictionary)
β”‚   β”‚   β”œβ”€β”€ config.js
β”‚   β”‚   β”œβ”€β”€ platforms/         # CSS, JS, iOS, Android, Figma
β”‚   β”‚   └── transforms/        # Custom transforms
β”‚   └── package.json
β”œβ”€β”€ components/                # React component library
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ primitives/        # Atoms: Button, Input, Badge, Avatar
β”‚   β”‚   β”œβ”€β”€ composites/        # Molecules: Card, Dropdown, Tabs
β”‚   β”‚   β”œβ”€β”€ patterns/          # Organisms: FormLayout, DataTable, Modal
β”‚   β”‚   β”œβ”€β”€ hooks/             # useTheme, useMediaQuery, useTokens
β”‚   β”‚   β”œβ”€β”€ utils/             # cn(), token helpers
β”‚   β”‚   └── index.ts           # Public API
β”‚   β”œβ”€β”€ stories/               # Storybook stories
β”‚   β”œβ”€β”€ tests/                 # Vitest + RTL + a11y
β”‚   β”œβ”€β”€ package.json
β”‚   └── tsconfig.json
β”œβ”€β”€ docs/                      # Documentation site (Astro/Next.js)
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ pages/             # Getting started, components, tokens
β”‚   β”‚   β”œβ”€β”€ components/        # Doc-specific components
β”‚   β”‚   └── content/           # MDX content
β”‚   └── package.json
β”œβ”€β”€ figma/                     # Figma sync
β”‚   β”œβ”€β”€ tokens/                # Tokens Studio / Figma Variables export
β”‚   β”œβ”€β”€ components/            # Component mapping docs
β”‚   └── sync-script.js         # CI sync validation
β”œβ”€β”€ .github/
β”‚   └── workflows/             # CI: build, test, publish, sync check
β”œβ”€β”€ turbo.json                 # Turborepo config
β”œβ”€β”€ package.json               # Root workspace
└── README.md

**Monorepo (Turborepo)** β€” Independent versioning, shared tooling, fast builds.

---

Layer 1: Design Tokens (The Source of Truth)

Token Structure (Style Dictionary Format)

// tokens/src/color.json
{
  "color": {
    "primary": {
      "50": { "value": "#eff6ff", "type": "color" },
      "100": { "value": "#dbeafe", "type": "color" },
      "500": { "value": "#3b82f6", "type": "color" },
      "600": { "value": "#2563eb", "type": "color" },
      "700": { "value": "#1d4ed8", "type": "color" },
      "900": { "value": "#1e3a8a", "type": "color" }
    },
    "neutral": {
      "50": { "value": "#fafafa", "type": "color" },
      "100": { "value": "#f5f5f5", "type": "color" },
      "900": { "value": "#171717", "type": "color" }
    },
    "semantic": {
      "background": { "value": "{color.neutral.50}", "type": "color" },
      "foreground": { "value": "{color.neutral.900}", "type": "color" },
      "border": { "value": "{color.neutral.200}", "type": "color" },
      "ring": { "value": "{color.primary.500}", "type": "color" },
      "error": { "value": "{color.red.500}", "type": "color" },
      "success": { "value": "{color.green.500}", "type": "color" }
    }
  }
}
// tokens/src/typography.json
{
  "typography": {
    "fontFamily": {
      "sans": { "value": "Inter, system-ui, sans-serif", "type": "fontFamily" },
      "mono": { "value": "JetBrains Mono, monospace", "type": "fontFamily" }
    },
    "fontSize": {
      "xs": { "value": "0.75rem", "type": "fontSize" },
      "sm": { "value": "0.875rem", "type": "fontSize" },
      "base": { "value": "1rem", "type": "fontSize" },
      "lg": { "value": "1.125rem", "type": "fontSize" },
      "xl": { "value": "1.25rem", "type": "fontSize" },
      "2xl": { "value": "1.5rem", "type": "fontSize" },
      "3xl": { "value": "1.875rem", "type": "fontSize" },
      "4xl": { "value": "2.25rem", "type": "fontSize" }
    },
    "fontWeight": {
      "normal": { "value": "400", "type": "fontWeight" },
      "medium": { "value": "500", "type": "fontWeight" },
      "semibold": { "value": "600", "type": "fontWeight" },
      "bold": { "value": "700", "type": "fontWeight" }
    },
    "lineHeight": {
      "tight": { "value": "1.25", "type": "lineHeight" },
      "normal": { "value": "1.5", "type": "lineHeight" },
      "relaxed": { "value": "1.75", "type": "lineHeight" }
    }
  }
}

Build: Style Dictionary β†’ Multi-Platform

// tokens/build/config.js
const StyleDictionary = require('style-dictionary');

StyleDictionary.registerTransform({ name: 'css/custom-property', type: 'value', matcher: () => true, transformer: (prop) => `var(--${prop.path.join('-')})` });

const config = { source: ['src/**/*.json'], platforms: { css: { transformGroup: 'css', buildPath: 'dist/css/', files: [{ destination: 'tokens.css', format: 'css/variables', options: { outputReferences: true } }] }, js: { transformGroup: 'js', buildPath: 'dist/js/', files: [{ destination: 'tokens.js', format: 'javascript/es6', options: { outputReferences: true } }] }, ts: { transformGroup: 'js', buildPath: 'dist/ts/', files: [{ destination: 'tokens.ts', format: 'typescript/es6-declarations', options: { outputReferences: true } }] }, figma: { transformGroup: 'figma', buildPath: 'dist/figma/', files: [{ destination: 'tokens.json', format: 'figma/tokens' }] } } };

module.exports = config;

**Output (CSS):**

/* tokens/dist/css/tokens.css */
:root {
  --color-primary-50: #eff6ff;
  --color-primary-500: #3b82f6;
  --color-primary-600: #2563eb;
  --color-semantic-background: var(--color-neutral-50);
  --color-semantic-foreground: var(--color-neutral-900);
  --typography-font-size-xs: 0.75rem;
  --typography-font-size-base: 1rem;
  --spacing-4: 1rem;
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}

---

Layer 2: Components (React + TypeScript + Tailwind)

Primitive: Button (Accessible, Variants, Composable)

// components/src/primitives/Button/Button.tsx
'use client';

import { forwardRef } from 'react'; import { Slot } from '@radix-ui/react-slot'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '../../utils/cn'; import { buttonVariants } from './button.variants';

export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; loading?: boolean; }

export const Button = forwardRef( ({ className, variant, size, asChild = false, loading, disabled, children, ...props }, ref) => { const Comp = asChild ? Slot : 'button'; return ( aria-busy={loading} {...props} > {loading && ( aria-hidden="true"> )} {children} ); } ); Button.displayName = 'Button';

// components/src/primitives/Button/button.variants.ts
import { cva } from 'class-variance-authority';

export const buttonVariants = cva( 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', }, size: { default: 'h-10 px-4 py-2', sm: 'h-9 rounded-md px-3', lg: 'h-11 rounded-md px-8', xl: 'h-12 rounded-lg px-10 text-base', icon: 'h-10 w-10', }, }, defaultVariants: { variant: 'default', size: 'default' }, } );

Composite: Card (Composable, Accessible)

// components/src/composites/Card/Card.tsx
'use client';

import { cn } from '../../utils/cn';

const Card = forwardRefReact.HTMLAttributes>( ({ className, ...props }, ref) => (

) ); Card.displayName = 'Card';

const CardHeader = forwardRefReact.HTMLAttributes>( ({ className, ...props }, ref) => (

) ); CardHeader.displayName = 'CardHeader';

const CardTitle = forwardRefReact.HTMLAttributes>( ({ className, ...props }, ref) => (

) ); CardTitle.displayName = 'CardTitle';

const CardContent = forwardRefReact.HTMLAttributes>( ({ className, ...props }, ref) => (

) ); CardContent.displayName = 'CardContent';

export { Card, CardHeader, CardTitle, CardContent };

Pattern: FormLayout (Opinionated, Accessible Defaults)

// components/src/patterns/FormLayout/FormLayout.tsx
'use client';

import { cn } from '../../utils/cn'; import { Label } from '../../primitives/Label'; import { Input } from '../../primitives/Input'; import { Textarea } from '../../primitives/Textarea'; import { Button } from '../../primitives/Button';

interface FormFieldProps { label: string; name: string; type?: 'text' | 'email' | 'tel' | 'url' | 'password'; required?: boolean; error?: string; hint?: string; children?: React.ReactNode; // For custom inputs (Select, DatePicker) }

function FormField({ label, name, type = 'text', required, error, hint, children }: FormFieldProps) { const errorId = `${name}-error`; const hintId = `${name}-hint`;

return (

{children ? ( <> {children} {error && } {hint && !error &&

{hint}

} ) : ( aria-invalid={!!error} aria-describedby={error ? errorId : hint ? hintId : undefined} className={error ? 'border-destructive focus:ring-destructive' : undefined} /> )}
); }

interface FormLayoutProps { fields: FormFieldProps[]; submitLabel: string; onSubmit: (data: Record) => void; loading?: boolean; secondaryAction?: React.ReactNode; }

export function FormLayout({ fields, submitLabel, onSubmit, loading, secondaryAction }: FormLayoutProps) { const [formData, setFormData] = useState>({});

const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSubmit(formData); };

return (

{fields.map(field => ( {field.children && React.cloneElement(field.children as React.ReactElement, { value: formData[field.name], onChange: (v: unknown) => setFormData(d => ({ ...d, [field.name]: v })), })} ))}
{secondaryAction}
); }

---

Layer 3: Figma ↔ Code Sync

Tokens Studio (Figma Plugin) β†’ Style Dictionary

  1. **Designers** manage tokens in Figma (Tokens Studio plugin)
  2. **Export** β†’ `figma/tokens/tokens.json`
  3. **CI** validates sync: `npm run tokens:sync:check`
  4. **Build** β†’ CSS/JS/TS outputs auto-generated
// figma/sync-script.js
const fs = require('fs');
const path = require('path');

const FIGMA_TOKENS = path.resolve(__dirname, 'tokens/tokens.json'); const CODE_TOKENS = path.resolve(__dirname, '../tokens/src');

function validateSync() { const figma = JSON.parse(fs.readFileSync(FIGMA_TOKENS, 'utf8')); const codeFiles = fs.readdirSync(CODE_TOKENS).filter(f => f.endsWith('.json'));

// Compare key token sets (color, spacing, typography) // Fail CI if drift > threshold }

validateSync();

Component Mapping Doc

| Figma Component | Code Component | Status | Notes | |-----------------|----------------|--------|-------| | Button/Primary | `Button{variant:'default'}` | βœ… Synced | | | Button/Secondary | `Button{variant:'secondary'}` | βœ… Synced | | | Input/Default | `Input` | βœ… Synced | | | Card/Elevated | `Card` | βœ… Synced | | | Modal/Default | `Dialog` | ⚠️ Gap | Figma has footer variant | | Table/Sortable | `DataTable` | ❌ Missing | Needs implementation |

**Sync checklist in PR template:**

  • [ ] Tokens updated in Figma β†’ exported
  • [ ] New components have Figma counterparts
  • [ ] Variant mapping documented
  • [ ] Design QA passed

---

Layer 4: Documentation (Astro + Starlight / Next.js)

// docs/src/pages/components/button.mdx
---
title: Button
component: Button
category: Primitives
---

import { ComponentPreview, PropsTable } from '@/components/docs';

Button

Interactive element for actions. Built on Radix Slot for polymorphic composition.

Usage

import { Button } from '@ui-designer/components';

Variants

Props

Accessibility

  • Keyboard: `Enter`/`Space` activates
  • Focus: Visible ring (WCAG 2.2 AA)
  • Loading: `aria-busy` + spinner announced
  • Disabled: Removed from tab order

---

Governance: The Process That Keeps It Alive

1. Ownership Model

| Role | Responsibility | |------|----------------| | **Design System Owner** (1 person) | Roadmap, prioritization, breaking changes | | **Design Contributors** (2-3) | Token updates, Figma components, design QA | | **Engineering Contributors** (2-3) | Component implementation, testing, releases | | **Consumers** (all teams) | Feedback, bug reports, adoption |

2. Contribution Workflow

  1. Request β†’ GitHub Issue (template: component / token / pattern)
  2. RFC β†’ Lightweight design doc (1-2 pages) for new components
  3. Build β†’ Branch β†’ PR with: component, stories, tests, docs
  4. Review β†’ Design QA + Code review + A11y check
  5. Merge β†’ Auto-publish to npm (changesets) + Storybook deploy
  6. Communicate β†’ Changelog + Slack announcement + migration guide

3. Versioning & Releases (Changesets)

# Contributor adds changeset
npx changeset
# Select: patch / minor / major
# Write summary: "Add Button loading state"

# Release (maintainer) npx changeset version # Updates versions, CHANGELOG.md git push --follow-tags npm publish --access public # Auto via CI

4. Adoption Metrics (Track Quarterly)

| Metric | Target | |--------|--------| | Component coverage | >80% of UI uses DS components | | Token usage | 100% of colors/spacing from tokens | | Breaking changes/year | <2 (major) | | redesign-checklist-50-steps" class="internal-link">Migration time | <1 day for consumers | | wcag-2026" class="internal-link">Accessibility regressions | 0 |

---

Tooling Stack (2026)

| Purpose | Tool | Why | |---------|------|-----| | **Tokens** | Style Dictionary | Multi-platform, mature, extensible | | **Figma Sync** | Tokens Studio | Variables support, two-way sync | | **Components** | React + TypeScript + Tailwind | Type-safe, utility-first, no runtime | | **Variants** | class-variance-authority (CVA) | Type-safe variants, no CSS-in-JS | | **Primitives** | Radix UI / cms-comparison-wordpress-sanity-strapi-2026" class="internal-link">Headless UI | Accessible, unstyled, composable | | **Documentation** | Astro Starlight / Next.js | Fast, MDX, component previews | | **Storybook** | Storybook 8 + Vite | Component development, visual testing | | **Testing** | Vitest + RTL + axe-core | Fast, accessible, CI-friendly | | **testing-strategies-2026" class="internal-link">Visual Regression** | Chromatic / Percy | Catch visual bugs | | **Monorepo** | Turborepo | Fast builds, caching, independent versions | | **Publishing** | Changesets + npm | Automated versioning, changelogs | | **CI** | GitHub Actions | Lint, typecheck, test, build, publish |

---

Migration: Adopting in Existing Codebase

Phase 1: Foundation (Weeks 1-2)

  • [ ] Set up monorepo + tokens package
  • [ ] Extract existing colors/spacing/typography β†’ tokens
  • [ ] Build CSS output β†’ import in global CSS
  • [ ] Replace hardcoded values with token references

Phase 2: Primitives (Weeks 3-6)

  • [ ] Button, Input, Label, Badge, Avatar, Icon
  • [ ] Storybook + a11y tests for each
  • [ ] Replace existing usages (codemod where possible)

Phase 3: Composites (Weeks 7-10)

  • [ ] Card, Dropdown, Tabs, Dialog, Tooltip, Toast
  • [ ] FormLayout, Table, Navigation components
  • [ ] Document patterns (when to use which)

Phase 4: Governance (Ongoing)

  • [ ] Figma sync established
  • [ ] Contribution process documented
  • [ ] Team training sessions
  • [ ] Quarterly audit + roadmap review

---

The UI Designer Design System Package

We deliver a **production-ready design system** tailored to your brand:

  • **Tokens**: Color, spacing, typography, motion, shadows β€” synced Figma ↔ Code
  • **40+ Components**: Primitives β†’ Composites β†’ Patterns (all accessible, tested)
  • **Documentation**: Live site with playground, props tables, accessibility notes
  • **Tooling**: Turborepo, Changesets, Storybook, CI/CD, Visual regression
  • **Governance**: Contribution model, versioning, migration guides, team training
  • **Handoff**: 2-week onboarding for your team

**Investment:** β‚Ή15-25L | **Timeline:** 8-12 weeks | **ROI:** 40-60% faster UI development, zero design drift

Start Building Consistent UI

Book a free design system assessment β€” we'll audit your current UI, identify the highest-leverage components to build first, and give you a phased roadmap.

ui-designer.in/design-system

Frequently Asked Questions

Frequently Asked Questions

How long does it take to build a design system from scratch?
8-12 weeks for a production-ready system with 40+ components, tokens, documentation, and governance. Phase 1 (tokens + primitives): 4-6 weeks. Phase 2 (composites + patterns): 4-6 weeks. Ongoing governance is permanent.

Frequently Asked Questions

Do I need a monorepo for a design system?
Highly recommended. Turborepo/Nx gives you independent versioning, shared tooling, fast builds, and clear boundaries between tokens, components, and docs. Single-repo with folders works for small teams but scales poorly.

Frequently Asked Questions

Should designers write code for the design system?
Designers should own tokens in Figma (Tokens Studio) and component specs. Engineers implement components. The sync (Figma ↔ Code) should be automated. Designers reviewing PRs in Storybook is ideal β€” they shouldn't write production React code.

Frequently Asked Questions

How do I handle breaking changes in a design system?
Semantic versioning + Changesets. Major version = breaking changes with migration guide. Deprecate in minor, remove in major. Provide codemods for common migrations. Communicate 3 months ahead. Never break without a migration path.

Frequently Asked Questions

What's the minimum viable design system?
Tokens (color, spacing, typography) + 8 primitives (Button, Input, Label, Badge, Avatar, Icon, Tooltip, Card) + documentation. This covers 80% of UI needs. Build composites only when patterns repeat 3+ times.

Frequently Asked Questions

How do I measure design system adoption?
Track: % of UI using DS components (target >80%), token usage in CSS (target 100%), breaking changes/year (<2 major), migration time for consumers (<1 day), accessibility regressions (0). Automate with stylelint + custom scripts.

Frequently Asked Questions

Should the design system be a separate npm package?
Yes, for multi-app organizations. Publish to private npm registry. Apps consume `@your-org/design-tokens` and `@your-org/ui-components`. For single-app, a local `packages/` folder in monorepo works fine.