You're a developer building a product. You know you need a design system, but you don't have a designer. Maybe you can't afford one yet. Maybe you're moving too fast to wait for design. Maybe you just want to ship.
The typical advice is "hire a designer." But that's not always realistic. This guide shows you how to build a functional, consistent design system yourself using systematic approaches, constraints, and practical tools—no design degree required.
What You Actually Need
Design systems feel overwhelming because they're presented as complete artifacts: hundreds of components, detailed specs, perfectly documented. But you don't need that on day one.
The Minimum Viable Design System
Core elements:
- Color palette — 5-8 semantic colors (brand, text, backgrounds, borders, status)
- Spacing scale — 6-8 values (4px, 8px, 16px, 24px, 32px, 48px, 64px)
- Typography — 4-6 font sizes with consistent line heights
- Border radius — 3-4 values (none, small, medium, large)
- Component primitives — Button, Input, Card, Layout components
That's it. Start here. You can expand later.
What You Don't Need Yet
- 50 shades of brand colors
- Every possible component variant
- Detailed design documentation
- Figma files
- Animation systems
- Illustration libraries
These come later, if at all. Most products succeed with minimal design systems.
Step 1: Steal Color Palettes
You're not a designer. Don't try to invent brand colors from scratch. Use proven palettes.
Option A: Tailwind Default Palette
Tailwind's colors are already well-balanced and accessible:
// Pick one primary color from Tailwind
const colors = {
brand: {
primary: '#3B82F6', // blue-500
secondary: '#64748B', // slate-500
},
text: {
primary: '#0F172A', // slate-900
secondary: '#64748B', // slate-500
tertiary: '#94A3B8', // slate-400
},
bg: {
primary: '#FFFFFF',
secondary: '#F8FAFC', // slate-50
},
border: {
primary: '#E2E8F0', // slate-200
},
};
Why this works:
- Colors are already tested for contrast
- Consistent visual weight across shades
- Familiar to other developers
Option B: Copy a Product You Admire
Find a SaaS product with clean UI. Use a color picker extension to extract their palette:
- Stripe — Blue/purple, professional
- Linear — Purple, modern
- Vercel — Black/white, minimal
- Notion — Subtle grays, friendly
Chrome Extension: ColorZilla
Pick the color picker, hover over UI elements, note hex values.
Option C: Use a Palette Generator
Tools that generate accessible color palettes:
- Coolors.co — Generate random palettes, lock colors you like
- Palettte.app — Upload a reference image, extracts palette
- Huemint — AI-generated color schemes
Process:
- Generate several palettes
- Pick the one that feels right for your product category (finance = blue, creative = purple, etc.)
- Extract 2-3 core colors
- Add semantic grays for text and borders
Step 2: Build a Systematic Spacing Scale
Spacing is where amateur UIs fall apart. Random padding and margins create visual noise.
Use a Mathematical Scale
Don't pick arbitrary values. Use multiples:
4px base:
const spacing = {
0: '0',
1: '0.25rem', // 4px
2: '0.5rem', // 8px
3: '0.75rem', // 12px
4: '1rem', // 16px
6: '1.5rem', // 24px
8: '2rem', // 32px
12: '3rem', // 48px
16: '4rem', // 64px
};
Rule: Every spacing value must be in this scale. No exceptions.
Why 4px? It divides evenly into common screen widths and feels natural on retina displays.
Apply Spacing Consistently
- Small components (buttons, inputs):
padding: spacing[2]orspacing[3] - Cards, panels:
padding: spacing[4]orspacing[6] - Page margins:
spacing[8]orspacing[12] - Gaps in layouts:
spacing[4]orspacing[6]
Before (chaotic):
<div className="p-[23px]">
<h2 className="mb-[15px]">Title</h2>
<p className="mt-[18px]">Content</p>
</div>
After (systematic):
<div className="p-6">
<h2 className="mb-4">Title</h2>
<p className="mt-4">Content</p>
</div>
The second example feels more cohesive because spacing relationships are consistent.
Step 3: Typography Without a Type Designer
Pick a Font Pairing
Use proven combinations instead of experimenting:
Safe pairings:
- Inter (everything) — Modern, readable, free
- System fonts (
-apple-system, BlinkMacSystemFont) — Fast, native feel - Roboto (body) + Roboto Slab (headings) — Professional
- Work Sans (headings) + Source Sans (body) — Friendly
Where to get fonts:
- Google Fonts
- Fontsource (self-hosted, no external requests)
Recommendation: Start with Inter for everything. It works for both headings and body text.
// tailwind.config.js
module.exports = {
theme: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
},
};
Define a Type Scale
Use a ratio-based scale (not random sizes):
const fontSize = {
xs: '0.75rem', // 12px
sm: '0.875rem', // 14px
base: '1rem', // 16px
lg: '1.125rem', // 18px
xl: '1.25rem', // 20px
'2xl': '1.5rem', // 24px
'3xl': '1.875rem', // 30px
'4xl': '2.25rem', // 36px
};
Usage rules:
- Body text:
text-base - Small labels:
text-sm - Section headings:
text-xlortext-2xl - Page titles:
text-3xlortext-4xl
Line heights: Match font sizes with appropriate line heights:
const lineHeight = {
tight: 1.25, // Headings
normal: 1.5, // Body text
relaxed: 1.75, // Spacious paragraphs
};
Step 4: Border Radius for Visual Cohesion
Border radius is a subtle signal of brand personality.
Conservative (finance, enterprise):
borderRadius: {
none: '0',
sm: '0.25rem',
base: '0.375rem',
}
Modern (SaaS, productivity):
borderRadius: {
none: '0',
sm: '0.5rem',
base: '0.75rem',
lg: '1rem',
}
Friendly (consumer, social):
borderRadius: {
base: '1rem',
lg: '1.5rem',
full: '9999px',
}
Rule: Pick one radius for most elements (cards, inputs, buttons), use it everywhere.
Step 5: Build Core Components
You need 5-7 primitive components. Everything else composes from these.
Button Component
// src/components/Button.tsx
type ButtonProps = {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
onClick?: () => void;
};
export function Button({
variant = 'primary',
size = 'md',
children,
onClick
}: ButtonProps) {
const variants = {
primary: 'bg-brand-primary text-white hover:bg-brand-secondary',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
outline: 'border border-gray-300 text-gray-700 hover:bg-gray-50',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
return (
<button
className={`${variants[variant]} ${sizes[size]} rounded-lg font-medium transition`}
onClick={onClick}
>
{children}
</button>
);
}
Input Component
// src/components/Input.tsx
type InputProps = {
label: string;
value: string;
onChange: (value: string) => void;
type?: 'text' | 'email' | 'password';
error?: string;
};
export function Input({ label, value, onChange, type = 'text', error }: InputProps) {
return (
<div className="space-y-1">
<label className="block text-sm font-medium text-gray-700">
{label}
</label>
<input
type={type}
value={value}
onChange={(e) => onChange(e.target.value)}
className={`
w-full px-3 py-2 border rounded-lg text-base
${error ? 'border-red-500' : 'border-gray-300'}
focus:outline-none focus:ring-2 focus:ring-blue-500
`}
/>
{error && <p className="text-sm text-red-600">{error}</p>}
</div>
);
}
Card Component
// src/components/Card.tsx
type CardProps = {
children: React.ReactNode;
padding?: 'sm' | 'md' | 'lg';
};
export function Card({ children, padding = 'md' }: CardProps) {
const paddingClasses = {
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
};
return (
<div className={`bg-white border border-gray-200 rounded-lg ${paddingClasses[padding]}`}>
{children}
</div>
);
}
Layout Components
// src/components/Container.tsx
export function Container({ children, size = 'lg' }) {
const sizes = {
sm: 'max-w-3xl',
md: 'max-w-5xl',
lg: 'max-w-7xl',
};
return (
<div className={`${sizes[size]} mx-auto px-4 sm:px-6 lg:px-8`}>
{children}
</div>
);
}
// src/components/Stack.tsx
export function Stack({ children, spacing = 4, direction = 'vertical' }) {
const classes = direction === 'vertical'
? `flex flex-col gap-${spacing}`
: `flex flex-row gap-${spacing}`;
return <div className={classes}>{children}</div>;
}
Step 6: Use AI to Fill Gaps
When you need a component you haven't built, use AI with constraints:
Provide Design System Context
Create a Modal component using our design system:
Colors:
- Background: bg-white
- Overlay: bg-black/50
- Border: border-gray-200
Spacing:
- Padding: p-6
- Gap between elements: gap-4
Border radius: rounded-lg
Make it match the style of our Button and Card components.
Validate AI Output
Check generated code against your token rules:
- Uses only approved colors
- Spacing follows scale
- Border radius matches system
- Typography uses defined sizes
If AI hallucinates (uses bg-blue-500 instead of bg-brand-primary), correct it:
Update the component to use bg-brand-primary instead of bg-blue-500.
Step 7: Document as You Go
You don't need comprehensive documentation. You need enough to stay consistent.
Create a Simple Reference
# Design System
## Colors
- Primary: #3B82F6 (use `bg-brand-primary`, `text-brand-primary`)
- Gray scale: slate (50, 100, 200, 300, 400, 500, 700, 900)
## Spacing
Use only: 1, 2, 3, 4, 6, 8, 12, 16
Example: `p-4`, `gap-6`, `mt-8`
## Typography
- Body: text-base
- Small: text-sm
- Headings: text-xl, text-2xl, text-3xl
## Components
- Button: `<Button variant="primary" size="md">`
- Input: `<Input label="Email" value={email} onChange={setEmail}>`
- Card: `<Card padding="md">`
## Patterns
- Cards: `bg-white border border-gray-200 rounded-lg p-6`
- Buttons: `bg-brand-primary text-white px-4 py-2 rounded-lg`
- Forms: Stack inputs with `gap-4`
Save as DESIGN_SYSTEM.md in your repo root. Reference it when building new components.
Step 8: Enforce Consistency
Use Tailwind Config to Constrain Choices
// tailwind.config.js
module.exports = {
theme: {
colors: {
brand: {
primary: '#3B82F6',
secondary: '#64748B',
},
text: {
primary: '#0F172A',
secondary: '#64748B',
},
// Only the colors you need
},
spacing: {
// Override to only allow approved values
0: '0',
1: '0.25rem',
2: '0.5rem',
3: '0.75rem',
4: '1rem',
6: '1.5rem',
8: '2rem',
12: '3rem',
16: '4rem',
},
// Disable unused values
borderWidth: {
DEFAULT: '1px',
2: '2px',
},
},
};
This makes it impossible to use non-system values in Tailwind classes.
Add ESLint Rules
// .eslintrc
{
"rules": {
"tailwindcss/no-custom-classname": "warn"
}
}
Warns when you use classes that don't exist in your Tailwind config.
Tools That Accelerate Solo Design System Building
Shadcn UI (Component Foundation)
Shadcn UI provides copy-paste components built on Radix UI:
npx shadcn-ui@latest init
npx shadcn-ui@latest add button
npx shadcn-ui@latest add input
npx shadcn-ui@latest add card
You get accessible, production-ready components that you can customize to match your tokens.
FramingUI (Token-Driven System)
FramingUI is built for developers without designers:
- Define tokens once (colors, spacing, typography)
- Generates Tailwind config automatically
- Provides type-safe components
- AI tools (Cursor, Claude Code) automatically use your tokens
Instead of manually building token infrastructure, you define your palette and spacing, and the system enforces consistency across all code—human or AI-generated.
Tailwind UI (Premium Components)
Tailwind UI ($300) gives you 500+ component examples:
- Copy, paste, customize
- Already follows Tailwind best practices
- Responsive and accessible
Good investment if you need to move fast and want professional-looking UI.
Real-World Example: Building a SaaS Dashboard
Goal: Build a metrics dashboard without a designer.
1. Pick Palette
Use Tailwind's blue (primary), slate (grays):
colors: {
brand: { primary: '#3B82F6' },
text: { primary: '#0F172A', secondary: '#64748B' },
bg: { primary: '#FFFFFF', secondary: '#F8FAFC' },
border: { primary: '#E2E8F0' },
}
2. Build Primitive Components
Card(white bg, gray border, rounded-lg, p-6)Button(blue primary, slate secondary)Container(max-w-7xl, padding)
3. Compose Dashboard
export function Dashboard() {
return (
<Container>
<Stack spacing={8}>
<h1 className="text-3xl font-bold text-text-primary">Dashboard</h1>
<div className="grid grid-cols-3 gap-6">
<MetricCard title="Revenue" value="$45,231" change="+12%" />
<MetricCard title="Users" value="1,234" change="+8%" />
<MetricCard title="Conversion" value="3.2%" change="-2%" />
</div>
<Card>
<h2 className="text-xl font-semibold mb-4">Recent Activity</h2>
{/* Content */}
</Card>
</Stack>
</Container>
);
}
Result
Clean, consistent UI built in a few hours. No designer needed.
When to Eventually Hire a Designer
You can get far without a designer, but eventually you'll hit limits:
Hire when:
- Your product is generating revenue (justify the cost)
- You need brand identity (logo, illustrations, marketing)
- Users complain about UX (not just styling)
- Competitors have noticeably better UI
Don't hire when:
- You're pre-revenue and need to ship fast
- Your users are developers (they value function > form)
- Budget is constrained
- You just need "good enough" UI
Next Steps
- Pick a color palette — Steal from Tailwind or a product you admire
- Define spacing scale — Use 4px or 8px base
- Choose typography — Start with Inter for everything
- Build 5 primitives — Button, Input, Card, Container, Stack
- Document tokens — Create
DESIGN_SYSTEM.md - Enforce with tooling — Constrain Tailwind config, add ESLint rules
- Use AI strategically — Let it fill gaps using your system constraints
You don't need to be a designer to build a design system. You need constraints, consistency, and practical tools. Start small. Ship fast. Expand when it matters.
A systematic approach beats artistic intuition when you're building alone.