Handbook
/
Design & UX
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
AIMake has access to all of this
Our AI has access to the entire Startup Handbook. Ask it anything about building your startup.
Get started
Previous
Design for Non-Designers: Principles That Work
Next
Mobile UX Patterns Every Founder Should Know