Buttons with four different border-radius values. Three shades of blue that were each the "right" one at the time. A color palette that expanded organically until it stopped making sense. This is the natural outcome of building UI without a system.
You don't need a designer to fix it. You need a set of explicit decisions, written down in a format that prevents you from contradicting yourself six weeks later. That's what design tokens give you—and you can set them up in an afternoon.
Why Consistency Drifts
Solo development has a specific failure mode. You're the only person making style decisions, which means you're also the only person who would catch inconsistencies. When you're three hours deep on a feature at midnight, you're not auditing color values. You write what looks right and move on.
Day 1: <button className="bg-blue-500 px-4 py-2 rounded">Submit</button>
Day 15: <button className="bg-blue-600 px-6 py-3 rounded-lg">Continue</button>
Day 30: <button style={{ background: '#3B82F6', padding: '12px 24px' }}>Next</button>
All three were "correct" at the time. Together they produce a product that looks like it was built by three different people. Past-you, present-you, and 3am-you are now your design committee.
A design token solves this by removing the decision from the point of use. When you write var(--color-brand-primary), you're not choosing a color—you've already chosen it. The token is the decision, made once, applied everywhere.
Setting Up Your Token Foundation
Your design token foundation has two layers: a base palette of raw color values, and a semantic layer that assigns meaning to those values.
Start with the semantic layer. That's where the real value comes from.
/* globals.css */
:root {
/* Brand */
--color-brand-primary: oklch(0.50 0.15 240);
--color-brand-primary-hover: oklch(0.44 0.14 240);
/* Surfaces */
--color-surface-page: #FAFAFA;
--color-surface-card: #FFFFFF;
--color-surface-elevated: #FFFFFF;
/* Text */
--color-text-primary: #111827;
--color-text-secondary: #6B7280;
/* Borders */
--color-border-default: #E5E7EB;
/* Spacing scale */
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
--spacing-4: 1rem;
--spacing-6: 1.5rem;
--spacing-8: 2rem;
/* Radius */
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
}
[data-theme="dark"] {
--color-surface-page: #111827;
--color-surface-card: #1F2937;
--color-surface-elevated: #374151;
--color-text-primary: #F9FAFB;
--color-text-secondary: #9CA3AF;
--color-border-default: #374151;
}
This CSS lives in one file. Dark mode is handled by a single attribute swap on the root element. Every component that uses these variables adapts automatically.
Using Tokens in Components
Once your tokens are defined, use them consistently. The goal is zero hardcoded color values in component files.
// components/Card.tsx
export function Card({ children }: { children: React.ReactNode }) {
return (
<div
style={{
background: 'var(--color-surface-card)',
border: '1px solid var(--color-border-default)',
borderRadius: 'var(--radius-md)',
padding: 'var(--spacing-6)',
}}
>
{children}
</div>
);
}
If you're using Tailwind, map your tokens into the config:
// tailwind.config.js
export default {
theme: {
extend: {
colors: {
brand: {
primary: 'var(--color-brand-primary)',
primaryHover: 'var(--color-brand-primary-hover)',
},
surface: {
page: 'var(--color-surface-page)',
card: 'var(--color-surface-card)',
elevated: 'var(--color-surface-elevated)',
},
text: {
primary: 'var(--color-text-primary)',
secondary: 'var(--color-text-secondary)',
},
},
spacing: {
1: 'var(--spacing-1)',
2: 'var(--spacing-2)',
4: 'var(--spacing-4)',
6: 'var(--spacing-6)',
8: 'var(--spacing-8)',
},
borderRadius: {
sm: 'var(--radius-sm)',
md: 'var(--radius-md)',
lg: 'var(--radius-lg)',
},
},
},
}
Now bg-surface-card and text-text-primary are valid Tailwind classes backed by your tokens.
Connecting AI Tools to Your Design System
Once you have tokens, you can wire AI tools to use them. This is where the investment pays off disproportionately—every component an AI generates can be on-brand by default.
FramingUI ships an MCP server that surfaces your design system context directly to Claude Code and other AI tools. The recommended setup:
npx -y @framingui/mcp-server@latest init
This command detects your project, installs the necessary packages, and writes the MCP config. After restarting Claude Code, it has access to your token definitions and component catalog.
The .mcp.json it creates looks like this:
{
"mcpServers": {
"framingui": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@framingui/mcp-server@latest"]
}
}
}
With this in place, Claude Code can query your available tokens before generating a component. Instead of picking bg-blue-600 from its training data, it picks var(--color-brand-primary) from your system.
Color Without a Designer
The hardest part for developers new to design systems is picking a color palette that doesn't look arbitrary. OKLCH color space makes this more tractable.
OKLCH represents colors as lightness, chroma, and hue in a perceptually uniform way. Equal steps in lightness look like equal steps to the human eye, which is not true of HSL or hex values.
A workable primary scale:
:root {
--color-primary-50: oklch(0.95 0.03 240);
--color-primary-100: oklch(0.90 0.06 240);
--color-primary-500: oklch(0.50 0.15 240); /* main brand */
--color-primary-700: oklch(0.38 0.12 240);
--color-primary-900: oklch(0.25 0.08 240);
}
The hue value (240 in this example) sets the color family. Change it to 140 for green, 30 for orange, 290 for purple. The lightness and chroma relationships stay proportional.
WCAG contrast requirements are also easier to meet with OKLCH because lightness values map predictably to perceived contrast. Text at lightness 0.95 on a background at lightness 0.20 will be readable.
Changing Colors Later
This is the question that justifies the whole approach. When you want to change your brand color, you update the CSS variable in one place.
/* Before */
--color-brand-primary: oklch(0.50 0.15 240); /* blue */
/* After */
--color-brand-primary: oklch(0.50 0.15 140); /* green */
Every button, link, badge, and focus ring in the app updates. If a designer joins the team later and wants to refine the palette, they have a structured file to work with—not a codebase to grep through.
Design systems aren't for large teams. They're for anyone who expects their own code to still make sense six months from now. Set the foundation once, and the rules enforce themselves.