design system architecture requires explicit layer boundaries, automated pipelines, and version contracts. The following implementation path is framework-agnostic and optimized for engineering velocity.
Step 1: Define Architectural Boundaries
Separate concerns into four distinct layers:
- Tokens: Design variables (colors, spacing, typography, motion, breakpoints)
- Primitives: Unstyled, accessible base components (buttons, inputs, modals)
- Composites: Business-logic components built from primitives
- Adapters: Framework-specific wrappers (React, Vue, Svelte, Web Components)
Each layer must have independent versioning, testing, and distribution contracts. Primitives never import business logic. Tokens never import components.
Step 2: Implement a Deterministic Token Pipeline
Tokens should be authored in a single source of truth, transformed into platform-specific formats, and validated against design specs.
// packages/tokens/tokens.json
{
"color": {
"brand": {
"primary": { "value": "#0066FF", "type": "color" },
"secondary": { "value": "#00D4AA", "type": "color" }
},
"surface": {
"base": { "value": "#FFFFFF", "type": "color" },
"muted": { "value": "#F5F7FA", "type": "color" }
}
},
"spacing": {
"unit": { "value": "4", "type": "dimension" },
"small": { "value": "{spacing.unit.value} * 2", "type": "dimension" },
"medium": { "value": "{spacing.unit.value} * 4", "type": "dimension" }
}
}
Transform tokens using a pipeline that outputs CSS variables, SCSS maps, and JavaScript constants simultaneously:
// packages/tokens/build.config.js
import StyleDictionary from 'style-dictionary';
StyleDictionary.registerTransform({
name: 'css/variable',
type: 'value',
transformer: (token) => `var(--${token.path.join('-')})`
});
const sd = StyleDictionary.extend({
source: ['tokens.json'],
platforms: {
css: {
transformGroup: 'css',
buildPath: 'dist/css/',
files: [{ destination: 'variables.css', format: 'css/variables' }]
},
js: {
transformGroup: 'js',
buildPath: 'dist/js/',
files: [{ destination: 'tokens.js', format: 'javascript/es6' }]
}
}
});
await sd.buildAllPlatforms();
Primitives must be unstyled, composable, and accessibility-first. Implement them with explicit ARIA contracts and performance budgets.
// packages/primitives/src/Button.tsx
import { forwardRef } from 'react';
import { useButton } from '@react-aria/button';
import { mergeProps } from '@react-aria/utils';
export const Button = forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
(props, ref) => {
const { buttonProps } = useButton(props, ref);
return (
<button
{...mergeProps(buttonProps, props)}
ref={ref}
style={{ all: 'unset', cursor: 'pointer' }} // Unstyled base
/>
);
}
);
Button.displayName = 'Button';
Enforce performance budgets via build-time analysis:
// packages/primitives/package.json
{
"scripts": {
"analyze": "size-limit --why"
},
"size-limit": [
{ "path": "dist/index.js", "limit": "12 kB" },
{ "path": "dist/index.css", "limit": "8 kB" }
]
}
Step 4: Create Framework Adapters
Adapters translate primitives into framework-specific APIs without duplicating logic. They should be thin wrappers that map props, handle lifecycle, and expose framework utilities.
// packages/react-adapter/src/Button.tsx
import { Button as PrimitiveButton } from '@design-system/primitives';
import { composeProps } from '@design-system/utils';
export const Button = (props: React.ComponentProps<typeof PrimitiveButton>) => {
return <PrimitiveButton {...composeProps(props)} />;
};
Step 5: Establish Distribution & Version Contracts
Use semantic versioning with automated changelogs and deprecation paths. Implement a monorepo structure with workspace-aware tooling.
// package.json
{
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"release": "changeset publish",
"version": "changeset version"
}
}
Configure changesets to enforce version bumps and generate migration guides:
// .changeset/config.json
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [["**"]],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
Step 6: Automate Validation & Testing
Architecture fails without automated contracts. Implement:
- Token diff validation against Figma exports
- Visual regression testing per component
- Accessibility audit pipeline (axe-core, lighthouse)
- Bundle size enforcement on PR
# .github/workflows/ci.yml
name: Design System CI
on: [pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci
- run: npm run lint
- run: npm run test:unit
- run: npm run test:visual
- run: npm run analyze
- run: npm run validate:tokens
Pitfall Guide
-
Token-Only Thinking
Treating design tokens as static CSS variables ignores runtime behavior. Tokens must be versioned, validated, and transformed through a pipeline. Static variables drift from design specs and break cross-platform consistency.
-
Framework Coupling at the Core
Embedding React, Vue, or Svelte logic into primitives locks the system to a single ecosystem. Primitives must remain framework-agnostic. Adapters handle framework-specific lifecycles and prop mappings.
-
Skipping Version Contracts & Deprecation Paths
Breaking changes without migration guides or deprecation cycles cause consumer abandonment. Implement semantic versioning, automated changelogs, and @deprecated JSDoc annotations with fallback implementations.
-
Documentation Drift
Static markdown documentation diverges from code within weeks. Treat documentation as code: generate API docs from TypeScript types, sync examples with component source, and enforce documentation updates in CI.
-
Ignoring Performance Budgets
Design systems accumulate unused CSS and JavaScript when components are shipped without tree-shaking guarantees. Enforce bundle size limits, require explicit exports, and validate CSS specificity depth.
-
Over-Abstraction
Creating infinite component layers (e.g., BaseButton β StyledButton β PrimaryButton β ProductButton) increases cognitive load and maintenance cost. Limit composition depth to three layers: primitive, composite, product-specific.
-
No Feedback Loop from Consumers
Architecture that doesn't track adoption, error rates, or usage patterns becomes a black box. Implement telemetry (opt-in), issue templates, and quarterly architecture reviews with consuming teams.
Production Bundle
Action Checklist
Decision Matrix
| Criteria | Monolithic | Modular (Token-Driven) | Federated |
|---|
| Update Velocity | Low (coordinated releases) | High (independent packages) | Medium (contract-dependent) |
| Bundle Efficiency | Poor (unused code inclusion) | High (tree-shakeable) | High (scoped packages) |
| Cross-Team Alignment | Strong (central control) | Medium (shared contracts) | Weak (independent evolution) |
| Implementation Complexity | Low | Medium | High |
| Best For | Small teams, single product | Mid-to-large teams, multiple products | Enterprise, independent product lines |
Configuration Template
Copy-ready monorepo structure with token pipeline, workspace config, and CI enforcement.
design-system/
βββ package.json
βββ turbo.json
βββ .changeset/
β βββ config.json
βββ packages/
β βββ tokens/
β β βββ tokens.json
β β βββ build.config.js
β βββ primitives/
β β βββ src/
β β βββ package.json
β β βββ tsconfig.json
β βββ react-adapter/
β β βββ src/
β β βββ package.json
β βββ docs/
β βββ .storybook/
β βββ package.json
βββ .github/
β βββ workflows/
β βββ ci.yml
βββ README.md
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
"lint": { "outputs": [] },
"test": { "outputs": [] },
"analyze": { "outputs": [] },
"validate:tokens": { "outputs": [] }
}
}
// packages/primitives/tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"strict": true,
"jsx": "react-jsx",
"skipLibCheck": true,
"paths": { "@design-system/tokens": ["../tokens/dist/js/tokens.js"] }
},
"include": ["src/**/*"]
}
Quick Start Guide
-
Initialize Monorepo
Run npx create-turbo@latest design-system and scaffold workspace directories for tokens, primitives, react-adapter, and docs.
-
Configure Token Pipeline
Add style-dictionary to the tokens package. Author tokens.json, configure multi-format transforms, and run npm run build to generate CSS/JS outputs.
-
Build Unstyled Primitives
Implement base components using @react-aria or @floating-ui for accessibility and positioning. Export explicit types, enforce all: unset styling, and set size limits in package.json.
-
Wire CI & Versioning
Add .changeset for semantic versioning, configure GitHub Actions for lint/test/analyze/validate, and enforce documentation generation from TypeScript types.
-
Publish & Consume
Run npm run release to publish packages. In consuming apps, install via workspace or npm, import primitives, and apply framework adapters. Monitor adoption and iterate contracts quarterly.
Design system architecture is engineering infrastructure, not a UI polish layer. When built with explicit boundaries, automated pipelines, and version contracts, it becomes a compounding asset that accelerates product development, reduces regression risk, and enforces consistency at scale. Treat it as a platform, not a project.