How I approached creating a cohesive design system with tokens, components, and documentation — from first principles to production.
Every product team eventually faces the same challenge: inconsistency. Buttons look different across pages. Spacing feels random. Colors drift from the brand guidelines. The codebase becomes a graveyard of one-off components.
A design system solves this by creating a single source of truth — a shared language between designers and developers that scales with your team.
Design tokens are the atomic building blocks of your system. They represent the smallest design decisions: colors, spacing, typography, and more.
Here's how I structured the token system:
// tokens/colors.ts
export const colors = {
primary: {
50: '#F5FFCC',
100: '#F0FFB8',
200: '#E5FF8F',
300: '#DBFF66',
400: '#D0FF3D',
500: '#BFFF00', // Primary lime
600: '#99CC00',
700: '#739900',
800: '#4D6600',
900: '#263300',
},
neutral: {
0: '#FFFFFF',
50: '#F7F7F7',
100: '#E3E3E3',
200: '#C8C8C8',
800: '#1A1A1A',
900: '#0A0A0A',
1000: '#000000',
},
} as const;
export type ColorToken = typeof colors;
The as const assertion is crucial — it preserves literal types so TypeScript can catch invalid token references at compile time.
I use an 8px base grid with a geometric progression for larger values:
:root {
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-12: 3rem; /* 48px */
--space-16: 4rem; /* 64px */
--space-24: 6rem; /* 96px */
--space-32: 8rem; /* 128px */
}
Tip: Avoid arbitrary spacing values. Constraining yourself to a scale forces visual rhythm and makes layouts feel intentional.
Each component in the system follows a strict folder structure:
components/
├── Button/
│ ├── Button.tsx # Component implementation
│ ├── Button.styles.ts # Styled variants
│ ├── Button.test.tsx # Unit tests
│ ├── Button.stories.tsx # Storybook stories
│ └── index.ts # Public API
├── Input/
│ ├── Input.tsx
│ └── ...
└── index.ts # Barrel exports
Here's a real example from the system — a polymorphic button with variant support:
import { cva, type VariantProps } from 'class-variance-authority';
import { forwardRef, type ButtonHTMLAttributes } from 'react';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center font-mono font-bold uppercase tracking-wide transition-all duration-200 border-4 border-black',
{
variants: {
variant: {
primary: 'bg-lime text-black hover:bg-black hover:text-lime',
secondary: 'bg-white text-black hover:bg-black hover:text-white',
ghost: 'bg-transparent text-black border-transparent hover:border-black',
},
size: {
sm: 'px-4 py-2 text-xs',
md: 'px-6 py-3 text-sm',
lg: 'px-8 py-4 text-base',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
ref={ref}
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
}
);
Button.displayName = 'Button';
export { Button, buttonVariants };
Typography is where most design systems fall apart. The key is establishing a type scale with clear hierarchy:
// tokens/typography.ts
export const typography = {
display: {
fontFamily: 'var(--font-playfair)',
sizes: {
'2xl': { fontSize: '4.5rem', lineHeight: '1', fontWeight: 900 },
'xl': { fontSize: '3.75rem', lineHeight: '1', fontWeight: 900 },
'lg': { fontSize: '3rem', lineHeight: '1.1', fontWeight: 900 },
'md': { fontSize: '2.25rem', lineHeight: '1.2', fontWeight: 900 },
'sm': { fontSize: '1.875rem', lineHeight: '1.2', fontWeight: 900 },
},
},
body: {
fontFamily: 'var(--font-jetbrains)',
sizes: {
'lg': { fontSize: '1.125rem', lineHeight: '1.75' },
'md': { fontSize: '1rem', lineHeight: '1.75' },
'sm': { fontSize: '0.875rem', lineHeight: '1.5' },
'xs': { fontSize: '0.75rem', lineHeight: '1.5' },
},
},
} as const;
clamp()Instead of breakpoint-based sizing, I use CSS clamp() for smooth scaling:
.heading-display {
font-size: clamp(2.5rem, 5vw + 1rem, 6rem);
line-height: 1;
font-weight: 900;
}
This gives you a minimum size of 2.5rem, a preferred size that scales with the viewport, and a maximum of 6rem — all in one declaration.
After building and maintaining this system across three products, here are the hard-won insights:
Start small. Don't try to build everything upfront. Begin with colors, typography, and a few core components.
Document as you go. If a component isn't documented, it doesn't exist. Use Storybook or a similar tool.
Enforce constraints. The value of a design system is in what it prevents, not just what it provides.
Version your tokens. Treat design tokens like an API. Breaking changes should be versioned and communicated.
Measure adoption. Track which components are being used and which are being bypassed with custom code.
In the next post, I'll cover how to automate design token synchronization between Figma and code using Style Dictionary and the Figma API. Stay tuned.
# Preview: the token pipeline
npx style-dictionary build --config ./tokens.config.json
# Output:
# ✓ css/variables.css
# ✓ ts/tokens.ts
# ✓ json/tokens.json
Thanks for reading. If you found this useful, share it with someone building their first design system.