A machine-readable design system foundation that prevents design drift across multi-session agentic builds — before writing a single line of UI code
Solo Founder with AI Coding Agent
You're building a product across 50+ agent sessions. Without pre-build planning, session 1's header, session 20's sidebar, and session 50's footer will look like three different sites. This blueprint prevents that by establishing contracts the agent reads every session.
Team Adopting AI-Assisted Development
Multiple developers using Cursor, Claude Code, or Copilot on the same codebase. Each agent session makes locally reasonable decisions. The result: 5 filter patterns, 10 heading sizes, 8 max-widths. This blueprint creates the shared contract all agents follow.
Retrofitting an Existing Codebase
Your site already has design drift from prior sessions. Before fixing anything, you need to establish the target system — the canonical tokens, components, and patterns — so the fix doesn't just create a new flavor of inconsistency.
Define Your Token Architecture (The Source of Truth)
Create a design tokens file following the W3C Design Tokens specification (2025.10). Use the three-layer hierarchy: Option Tokens (available palette), Decision Tokens (contextual application), Component Tokens (specific mappings). This file becomes the single source of truth — if a value isn't here, it doesn't ship. The critical insight from research: primitive tokens (blue-500) undermine AI effectiveness. Semantic tokens (color-button-background-brand) enable it. This 'semantic token gap' is the #1 failure point in AI-generated UIs.
// design-tokens.tokens.json (W3C DTCG format)
{
// Layer 1: Option Tokens — available palette (PRIVATE to the system)
"color": {
"$type": "color",
"primitives": {
"blue-500": { "$value": "#3b82f6", "$description": "Primary blue" },
"blue-600": { "$value": "#2563eb" },
"gray-900": { "$value": "#111827", "$description": "Darkest gray" },
"gray-100": { "$value": "#f3f4f6" },
"green-500": { "$value": "#22c55e" },
"red-500": { "$value": "#ef4444" }
},
// Layer 2: Decision Tokens — contextual meaning (PUBLIC)
"decisions": {
"background": { "$value": "{color.primitives.gray-900}" },
"surface": { "$value": "#1a1d2e" },
"text-primary": { "$value": "#f0f0f0" },
"text-secondary": { "$value": "#94a3b8" },
"accent": { "$value": "{color.primitives.blue-500}" },
"success": { "$value": "{color.primitives.green-500}" },
"error": { "$value": "{color.primitives.red-500}" }
},
// Layer 3: Component Tokens — specific UI mappings (PUBLIC)
"button": {
"primary-bg": { "$value": "{color.decisions.accent}" },
"primary-text": { "$value": "#ffffff" },
"ghost-bg": { "$value": "transparent" },
"ghost-text": { "$value": "{color.decisions.text-secondary}" }
}
},
"spacing": {
"$type": "dimension",
"page-top": { "$value": "48px", "$description": "Top padding for all pages except homepage" },
"page-bottom": { "$value": "80px" },
"page-sides": { "$value": "24px", "$description": "Horizontal padding on all containers" },
"section": { "$value": "48px", "$description": "Between content sections" },
"card-gap": { "$value": "16px" }
},
"typography": {
"$type": "fontFamily",
"sans": { "$value": "var(--font-geist-sans), system-ui, sans-serif" },
"mono": { "$value": "var(--font-geist-mono), ui-monospace, monospace" }
}
}Transform Tokens to CSS Variables (Build Step)
Use Style Dictionary to transform your W3C tokens into platform-specific outputs. For web, this generates CSS custom properties. The key: expose ONLY semantic tokens (Layer 2 + Layer 3) to your codebase. Keep primitives (Layer 1) private — this constrains the AI's decision surface.
// style-dictionary.config.js
const StyleDictionary = require("style-dictionary");
module.exports = {
source: ["design-tokens.tokens.json"],
platforms: {
css: {
transformGroup: "css",
buildPath: "src/styles/",
files: [
{
destination: "tokens.css",
format: "css/variables",
filter: (token) => {
// CRITICAL: Only expose decision + component tokens
// Keep primitives private to reduce AI decision surface
return !token.path.includes("primitives");
},
},
],
},
},
};
// Run: npx style-dictionary build
// Output: src/styles/tokens.css with :root { --color-background: #111827; ... }Create a Typed Design System File (Agent Contract)
Create a TypeScript constants file that mirrors your tokens. This serves two purposes: (1) TypeScript catches token misuse at build time, (2) agents can import and reference the type-safe constants. This file is the bridge between your token file and your components.
// src/lib/design-system.ts
// GENERATED FROM design-tokens.tokens.json — do not edit manually
export const DS = {
// Layout
maxWidth: { wide: "1200px", narrow: "860px" },
padding: { top: "48px", bottom: "80px", sides: "24px", section: "48px" },
// Typography
font: {
sans: "var(--font-geist-sans), system-ui, sans-serif",
mono: "var(--font-geist-mono), ui-monospace, monospace",
},
heading: {
h1Index: { size: "clamp(28px, 4vw, 40px)", weight: 600, spacing: "-0.02em" },
h1Detail: { size: "clamp(24px, 4vw, 36px)", weight: 600, spacing: "-0.02em" },
h1Editorial: { size: "clamp(28px, 4vw, 40px)", weight: 400, spacing: "-0.03em" },
h2: { size: "20px", weight: 600, spacing: "-0.01em" },
h3: { size: "14px", weight: 600, spacing: "0.02em", font: "mono" },
body: { size: "15px", weight: 400, lineHeight: 1.6 },
caption: { size: "12px", weight: 500, spacing: "0.04em", font: "mono" },
},
// Colors (reference CSS variables, not raw values)
color: {
bg: "var(--color-background)",
surface: "var(--color-surface)",
text1: "var(--color-text-primary)",
text2: "var(--color-text-secondary)",
accent: "var(--color-accent)",
},
// Page types (determines layout)
pageType: {
index: { maxWidth: "1200px", sidebar: "left-filter" },
detail: { maxWidth: "860px", sidebar: "right-related" },
editorial: { maxWidth: "860px", sidebar: "none" },
dashboard: { maxWidth: "1200px", sidebar: "none" },
},
} as const;
export type PageType = keyof typeof DS.pageType;Set Up Component Documentation (Storybook + MCP)
Install Storybook with the MCP addon. This exposes your component catalog as machine-readable JSON that agents can query — prop types, defaults, examples, documentation. The Component Manifest dramatically reduces token consumption vs. loading entire codebases (50K-100K tokens per task).
// .storybook/main.ts
import type { StorybookConfig } from "@storybook/nextjs";
const config: StorybookConfig = {
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-essentials",
"@storybook/addon-mcp", // Exposes component metadata to AI agents
],
// Enable Component Manifest — structured JSON for LLM consumption
experimentalComponentsManifest: true,
framework: "@storybook/nextjs",
};
export default config;
// Install:
// npx storybook@latest init
// npx storybook add @storybook/addon-mcp
// Write stories for your core components:
// src/components/Button/Button.stories.tsx
// src/components/Card/Card.stories.tsx
// etc.
// Each story becomes part of the manifest that agents can query.Write the Agent Context File (CLAUDE.md)
Create the context file that your AI agent reads every session. Keep it under 300 lines (frontier LLMs reliably follow 150-200 instructions). Reference design-system.ts by file path rather than copying values. Use progressive disclosure — store detailed guidance in referenced files. Key insight from HumanLayer: don't use Claude as a linter (use hooks), don't auto-generate this file (craft every line manually).
# In your CLAUDE.md or .cursorrules:
## Design System (MUST READ)
Design tokens: `src/styles/tokens.css` — ONLY use CSS variables from this file.
Design constants: `src/lib/design-system.ts` — import DS for all layout/typography values.
Component catalog: Run Storybook MCP to see available components and their props.
### Rules
- **Zero hardcoded hex.** Every color MUST use a CSS variable.
- **Two widths only.** 1200px (index/dashboard) or 860px (detail/editorial).
- **One heading scale.** See design-system.ts heading object. No other sizes.
- **Page types determine layout.** Index, Detail, Editorial, Dashboard. No mixing.
- **Token enforcement.** If a value isn't in tokens.css, add it there first.
### Before writing any component:
1. Check Storybook for existing components (via MCP)
2. Import DS from src/lib/design-system.ts
3. Use CSS variables from tokens.css, never raw values
4. Match the page type layout rulesDefine Page Type Rules (Structural Contract)
Create a page type taxonomy that determines all structural decisions. Every page on the site is ONE type. No exceptions. No mixing. This eliminates the most common source of design drift: inconsistent layout decisions across pages.
// src/lib/page-types.ts
import { DS } from "./design-system";
export type PageType = "index" | "detail" | "editorial" | "dashboard";
export const PAGE_RULES: Record<PageType, {
maxWidth: string;
sidebar: "left-filter" | "right-related" | "none";
sidebarWidth?: string;
breadcrumbs: boolean;
search: boolean;
sort: boolean;
pagination: boolean;
}> = {
index: {
maxWidth: DS.maxWidth.wide,
sidebar: "left-filter", // >20 items; inline pills for ≤20
sidebarWidth: "240px",
breadcrumbs: false, // Top-level, no breadcrumbs
search: true, // Contextual search above content
sort: true, // Always: Newest, Name A-Z, + type-specific
pagination: true, // 24 items/page, URL-based (?page=2)
},
detail: {
maxWidth: DS.maxWidth.narrow,
sidebar: "right-related",
sidebarWidth: "280px",
breadcrumbs: true, // Section / Item Title
search: false,
sort: false,
pagination: false,
},
editorial: {
maxWidth: DS.maxWidth.narrow,
sidebar: "none",
breadcrumbs: true,
search: false,
sort: false,
pagination: false,
},
dashboard: {
maxWidth: DS.maxWidth.wide,
sidebar: "none",
breadcrumbs: false,
search: false,
sort: false,
pagination: false,
},
};
// Usage in components:
// const rules = PAGE_RULES[pageType];
// <div style={{ maxWidth: rules.maxWidth, margin: "0 auto" }}>Set Up Visual Regression Baseline (Verification Foundation)
Capture baseline screenshots of every page BEFORE any agentic modification session. This is your 'before' that all future changes are compared against. Start with Playwright's free built-in VRT — it covers 80% of use cases.
// tests/visual-regression.spec.ts
import { test, expect } from "@playwright/test";
const PAGES = [
{ name: "homepage", path: "/" },
{ name: "tools-index", path: "/tools" },
{ name: "tool-detail", path: "/tools/claude-code" },
{ name: "blueprints-index", path: "/blueprints" },
{ name: "editorial", path: "/replacements/jira" },
// Add every page in your site
];
for (const page of PAGES) {
test(`visual regression: ${page.name}`, async ({ page: p }) => {
await p.goto(page.path);
await p.waitForLoadState("networkidle");
// Full page screenshot comparison
await expect(p).toHaveScreenshot(`${page.name}.png`, {
fullPage: true,
maxDiffPixelRatio: 0.01, // 1% tolerance for dynamic content
});
});
}
// Capture baselines: npx playwright test --update-snapshots
// Run checks: npx playwright test
// Baselines stored in tests/visual-regression.spec.ts-snapshots/Create Pre-Commit Enforcement (CI Gate)
Set up lint rules that prevent raw values from entering the codebase. This is the equivalent of 'wash your hands' — basic hygiene that prevents most drift. The rule: if it's not in tokens.css, it doesn't ship.
// eslint-rules/no-hardcoded-design-values.js
// Custom ESLint rule that bans raw color/size values in components
module.exports = {
meta: {
type: "problem",
docs: { description: "Disallow hardcoded design values in components" },
messages: {
noHardcodedHex: "Use a CSS variable instead of hardcoded hex '{{value}}'",
noHardcodedPx: "Use a design token instead of hardcoded '{{value}}'",
noHardcodedFont: "Use FONT_SANS or FONT_MONO from design-system.ts",
},
},
create(context) {
const HEX_REGEX = /#[0-9a-fA-F]{3,8}/;
const BANNED_FONTS = /Inter|Georgia|DM Serif|serif/i;
return {
Literal(node) {
if (typeof node.value === "string") {
if (HEX_REGEX.test(node.value)) {
context.report({ node, messageId: "noHardcodedHex", data: { value: node.value } });
}
if (BANNED_FONTS.test(node.value)) {
context.report({ node, messageId: "noHardcodedFont" });
}
}
},
TemplateLiteral(node) {
for (const quasi of node.quasis) {
if (HEX_REGEX.test(quasi.value.raw)) {
context.report({ node, messageId: "noHardcodedHex", data: { value: quasi.value.raw.match(HEX_REGEX)[0] } });
}
}
},
};
},
};
// .eslintrc.js — add the rule
// rules: { "no-hardcoded-design-values": "error" }Third-party libraries that ship their own CSS with hardcoded values
Isolate third-party styles using CSS layers (@layer vendor, theme). Override vendor values with your tokens in the theme layer. Never modify vendor CSS directly.
Agents that ignore CLAUDE.md instructions and use raw values anyway
CLAUDE.md is necessary but insufficient — it's one layer in a five-layer stack. The enforcement layer (ESLint rules, CI gates) catches what the context layer misses. If an agent bypasses context, the linter catches it before merge.
Design tokens file grows unwieldy (500+ tokens)
Split tokens by domain: color.tokens.json, spacing.tokens.json, typography.tokens.json. Style Dictionary can merge multiple source files. Keep the public API (semantic tokens) small even if the private palette is large.
New team member (human or AI) starts building without reading the design system
Storybook MCP is the safety net — agents that query the MCP get correct component APIs automatically. For humans: the CI gate blocks non-token values regardless of whether they read the docs. The system enforces itself.
Further reading
Related Concepts
Browse full Lexicon →