How to Create a Design System from Scratch
A design system creates consistency and speeds up development. Here's how to build one without overengineering.
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.
What a Design System Is
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.
Start with Tokens
Tokens are the foundation. Define these first:
Colors
/* 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.
Typography
/* 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;
Spacing
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 */
Radii and Shadows
/* 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);
Building Components
With tokens defined, build components that use them:
Button
// 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
// 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
// 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>
);
}
The Component Checklist
Every component should:
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
Using a UI Framework
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.
Documentation
As your system grows, document it:
For Each Component
•
What it’s for
•
When to use it
•
Props/variants available
•
Code examples
For Tokens
•
The complete color palette
•
Typography scale with examples
•
Spacing values with visual reference
For Patterns
•
How to combine components
•
Standard layouts
•
Common flows
Tools like Storybook can generate documentation from your code.
Evolution Strategy
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)
•
Add components as needed
•
Refine existing components
•
Start documenting patterns
Phase 3: Maturity (Later)
•
Full documentation
•
Contribution guidelines
•
Design-engineering sync process
Don’t try to build a comprehensive system upfront. Let it evolve with your needs.
Common Mistakes
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.
Key Takeaways
•
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
Put your context to work.
Keep your project’s decisions, next steps and proof together as your agents work.
Explore the agent workflow →