A design system is a collection of reusable components and standards that make building consistent interfaces faster. Without one, every new page or feature reinvents styling decisions. With one, teams move faster and products look polished.
But design systems can also become massive over-engineering projects. Here’s how to build one that helps rather than hinders.
At its core, a design system includes:
Design tokens: The fundamental values—colors, typography, spacing, shadows.
Components: Reusable UI pieces—buttons, inputs, cards, modals.
Patterns: How components combine to solve common problems.
Documentation: How and when to use everything.
For startups, you need tokens and components. Patterns and documentation come later.
Tokens are the foundation. Define these first:
/* Primitives */
--gray-50: #fafafa;
--gray-100: #f4f4f5;
--gray-200: #e4e4e7;
--gray-300: #d4d4d8;
--gray-400: #a1a1aa;
--gray-500: #71717a;
--gray-600: #52525b;
--gray-700: #3f3f46;
--gray-800: #27272a;
--gray-900: #18181b;
--blue-500: #3b82f6;
--blue-600: #2563eb;
/* Semantic */
--color-background: var(--gray-900);
--color-surface: var(--gray-800);
--color-border: var(--gray-700);
--color-text-primary: var(--gray-50);
--color-text-secondary: var(--gray-400);
--color-text-muted: var(--gray-500);
--color-primary: var(--blue-600);
Primitives are raw values. Semantic tokens describe purpose. Use semantic tokens in your code—they make theming and adjustments easier.
/* Font families */
--font-sans: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
/* Font sizes */
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */
--text-3xl: 1.875rem; /* 30px */
/* Line heights */
--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.625;
/* Font weights */
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
Use a scale with consistent ratios:
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-5: 1.25rem; /* 20px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-10: 2.5rem; /* 40px */
--space-12: 3rem; /* 48px */
--space-16: 4rem; /* 64px */
/* Border radius */
--radius-sm: 0.25rem; /* 4px */
--radius-md: 0.375rem; /* 6px */
--radius-lg: 0.5rem; /* 8px */
--radius-xl: 0.75rem; /* 12px */
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
With tokens defined, build components that use them:
// Button.tsx
type ButtonProps = {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
export function Button({
variant = 'primary',
size = 'md',
children,
...props
}: ButtonProps) {
const baseStyles = 'font-medium rounded-lg transition-colors';
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-700 text-gray-100 hover:bg-gray-600',
ghost: 'text-gray-300 hover:bg-gray-800',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base',
};
return (
<button
className={`${baseStyles} ${variants[variant]} ${sizes[size]}`}
{...props}
>
{children}
</button>
);
}
// Input.tsx
type InputProps = {
label?: string;
error?: string;
} & React.InputHTMLAttributes<HTMLInputElement>;
export function Input({ label, error, ...props }: InputProps) {
return (
<div className="space-y-1.5">
{label && (
<label className="block text-sm font-medium text-gray-300">
{label}
</label>
)}
<input
className={`
w-full px-3 py-2 bg-gray-800 border rounded-lg text-gray-100
placeholder:text-gray-500 focus:outline-none focus:ring-2
${error
? 'border-red-500 focus:ring-red-500'
: 'border-gray-700 focus:ring-blue-500'
}
`}
{...props}
/>
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
</div>
);
}
// Card.tsx
type CardProps = {
children: React.ReactNode;
className?: string;
};
export function Card({ children, className = '' }: CardProps) {
return (
<div className={`bg-gray-800 border border-gray-700 rounded-xl p-6 ${className}`}>
{children}
</div>
);
}
1.
Use tokens: No hardcoded colors, spacing, or sizes
2.
Be accessible: Proper ARIA attributes, keyboard navigation
3.
Handle states: Hover, focus, disabled, loading, error
4.
Be responsive: Work on all screen sizes
5.
Be composable: Work with other components
Component Library Structure
Organize your components:
src/
├── components/
│ ├── ui/
│ │ ├── Button.tsx
│ │ ├── Input.tsx
│ │ ├── Card.tsx
│ │ ├── Modal.tsx
│ │ └── index.ts
│ └── patterns/
│ ├── Form.tsx
│ ├── DataTable.tsx
│ └── index.ts
├── styles/
│ ├── tokens.css
│ └── globals.css
└── lib/
└── utils.ts
Building from scratch is educational but slow. Consider starting with:
Tailwind + shadcn/ui: Copy-paste components you own. Highly customizable.
Radix UI: Unstyled, accessible primitives. Add your own styles.
Chakra UI: Full component library with theming.
Headless UI: Unstyled components from Tailwind team.
These give you a foundation to customize rather than building everything.
As your system grows, document it:
•
The complete color palette
•
Typography scale with examples
•
Spacing values with visual reference
•
How to combine components
Tools like Storybook can generate documentation from your code.
Phase 1: Foundation (Week 1-2)
•
Define tokens (colors, typography, spacing)
•
Build 5-10 essential components
•
Use consistently across the app
Phase 2: Expansion (Ongoing)
•
Refine existing components
•
Start documenting patterns
Phase 3: Maturity (Later)
•
Design-engineering sync process
Don’t try to build a comprehensive system upfront. Let it evolve with your needs.
Over-engineering: Building 50 components before you need them. Build as you go.
No single source of truth: Tokens defined in multiple places. Keep them centralized.
Ignoring accessibility: Components that don’t work with keyboards or screen readers.
Too rigid: A system so strict it can’t handle new requirements. Build in flexibility.
No buy-in: Developers work around the system instead of using it. Make it easy to use.
•
Start with tokens: colors, typography, spacing, shadows
•
Build components that use tokens consistently
•
Use a UI framework as a foundation rather than starting from zero
•
Document as you go, but don’t over-document early
•
Let the system evolve—don’t try to predict every need upfront
•
Make the system easy to use so the team actually uses it