Introduction
Design systems help teams ship faster by reusing common UI components and styles. For indie hackers, a lightweight design system is a force multiplier: it reduces design debt, streamlines implementation, and helps maintain consistency as new features are added.
Unlike enterprise design systems (which can take months to build), an indie design system is minimal, practical, and focused on solving your immediate problems. It’s a living document that grows with your product.
This guide shows you how to build a practical design system that’s easy to maintain and adapts to your product’s needsโwithout the overhead.
What Makes a Good Indie Design System?
Lightweight
Focus only on your most used components and patterns. Start with 10โ15 core components rather than trying to build everything at once.
Practical
Designed for your real workflows and fast delivery. Your design system should answer “How do I build this feature faster?” not “How do I follow enterprise standards?”
Consistent
Clear tokens and constraints for colors, spacing, and typography. Consistency reduces cognitive load and makes your product feel intentional.
Documented
Short usage notes for each componentโnot lengthy specs. A one-paragraph usage guide beats a 50-page design manual.
Accessible
Built with keyboard navigation, semantic HTML, and WCAG 2.1 AA compliance in mind from day one. Accessibility isn’t an afterthought.
Key Design System Concepts
Design Tokens
Design tokens are the atomic units of your design system. They represent reusable values like colors, spacing, font sizes, and border radii.
Why tokens matter:
- Single source of truth for design decisions
- Easy to implement themes (light/dark mode)
- Faster to update globally (e.g., change primary color across 50 components)
- Enable consistency across design and code
Common token categories:
| Token Type | Examples | Use Case |
|---|---|---|
| Color | Primary, secondary, background, border, error | UI elements, text, backgrounds |
| Spacing | 4px, 8px, 12px, 16px… | Margins, padding, gaps |
| Typography | Font family, size, weight, line-height | Headings, body text, labels |
| Sizing | Button height, input height, icon size | Consistent dimensions |
| Radii | Small, medium, large border radius | Rounded corners |
| Shadows | Elevation levels (shadow-1, shadow-2) | Depth and hierarchy |
| Breakpoints | Mobile, tablet, desktop | Responsive design |
Example token file (JSON):
{
"colors": {
"primary": "#2563eb",
"secondary": "#64748b",
"background": "#ffffff",
"text": "#1e293b",
"border": "#e2e8f0",
"error": "#dc2626",
"success": "#16a34a"
},
"spacing": {
"xs": "4px",
"sm": "8px",
"md": "16px",
"lg": "24px",
"xl": "32px"
},
"typography": {
"fontFamily": "Inter, system-ui, sans-serif",
"fontSize": {
"sm": "12px",
"base": "14px",
"lg": "16px",
"xl": "18px",
"2xl": "24px"
}
}
}
Components
Components are reusable UI building blocks. They combine markup, styles, and behavior into a single unit.
Component hierarchy:
- Atomic (smallest): Button, input, icon, badge
- Molecular (combinations): Form field (label + input + error), card header, notification
- Organism (complex): Form, modal, navigation bar, data table
- Templates (layouts): Page layout with sidebar, dashboard grid
Focus on atomic and molecular components first. Organisms often vary too much per feature to be worth standardizing early.
Example: Button component structure
// Button.jsx
export function Button({
variant = 'primary',
size = 'md',
disabled = false,
children,
...props
}) {
return (
<button
className={`btn btn-${variant} btn-${size} ${disabled ? 'disabled' : ''}`}
disabled={disabled}
{...props}
>
{children}
</button>
);
}
// Usage
<Button variant="primary" size="lg">Sign Up</Button>
<Button variant="secondary" disabled>Disabled</Button>
Variants & States
Every component needs multiple states to handle different contexts.
States to consider:
- Interaction states: Default, hover, active, focus
- Disabled state: Greyed out, no interaction
- Loading state: Spinner or skeleton
- Error state: Red border, error icon, error message
- Success state: Green checkmark, confirmation message
Example: Input field states
/* Default */
.input {
border: 1px solid var(--color-border);
padding: var(--spacing-md);
}
/* Focus */
.input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
/* Error */
.input.error {
border-color: var(--color-error);
}
/* Disabled */
.input:disabled {
background-color: var(--color-disabled-bg);
color: var(--color-disabled-text);
cursor: not-allowed;
}
Spacing Scale
A consistent spacing scale creates visual harmony and reduces decision fatigue.
Why 8px scale is popular:
- Divides nicely (8, 16, 24, 32, 40, 48, 56, 64…)
- Aligns with common breakpoints (mobile, tablet, desktop)
- Works well with font sizes (14px, 16px, 18px, 20px…)
Example spacing scale:
xs: 4px (use sparingly)
sm: 8px
md: 16px
lg: 24px
xl: 32px
2xl: 48px
3xl: 64px
Apply consistently:
- Padding inside buttons: 8px horizontal, 4px vertical
- Card padding: 16px or 24px
- Section margins: 24px or 32px
- Gap between grid items: 16px
Tokens โ CSS Variables
CSS variables enable runtime theming and make token updates instant.
Benefits:
- Change primary color in one place
- Support light/dark mode without recompiling
- Allow user customization (e.g., “dark theme” toggle)
Example CSS variable setup:
/* tokens.css */
:root {
/* Colors */
--color-primary: #2563eb;
--color-secondary: #64748b;
--color-background: #ffffff;
--color-text: #1e293b;
--color-border: #e2e8f0;
--color-error: #dc2626;
/* Spacing */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
/* Typography */
--font-family: 'Inter', system-ui, sans-serif;
--font-size-base: 14px;
--font-size-lg: 16px;
--line-height-normal: 1.5;
/* Radii */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
/* 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.15);
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
:root {
--color-background: #0f172a;
--color-text: #f1f5f9;
--color-border: #1e293b;
}
}
Usage in CSS:
.button {
padding: var(--spacing-sm) var(--spacing-md);
background-color: var(--color-primary);
border-radius: var(--radius-md);
font-size: var(--font-size-base);
font-family: var(--font-family);
}
Step-by-Step: Build Your Indie Design System
Step 1: Audit Your Current UI
Before building anything, understand what you already have.
How to audit:
- Screenshot every unique component in your product
- Categorize: buttons, inputs, cards, modals, notifications, etc.
- Identify patterns: What components repeat? What variations exist?
- Spot inconsistencies: “Why is this button 32px and that one 40px?”
What to look for:
- Duplicate components with slight variations (consolidate)
- Multiple color shades doing the same job (use tokens)
- Inconsistent spacing and sizing (normalize to scale)
- Accessibility issues (focus states, contrast, semantic HTML)
Pro tip: Use Figma Audit or Sketch Plugins to quickly document your UI.
Step 2: Create Design Tokens
Start with color, spacing, and typography. You can add more token types later.
Step-by-step:
-
Define your color palette
- Primary color (main brand color, CTAs)
- Secondary color (supporting actions)
- Neutral colors (backgrounds, borders, text)
- Semantic colors (error, success, warning)
- Decide on light and dark mode variants
-
Create a spacing scale
- Use 4px or 8px as your base unit
- Create 8โ10 steps (xs, sm, md, lg, xl…)
- Apply consistently to margins, padding, gaps
-
Define typography
- Choose 1โ2 font families (serif + sans, or just sans)
- Define 5โ7 font sizes (sm, base, lg, xl, 2xl…)
- Set line-height (1.4 for headings, 1.5โ1.6 for body)
- Define font weights (400 normal, 600 semi-bold, 700 bold)
-
Add supporting tokens
- Border radius: small (4px), medium (6px), large (8px)
- Shadows: subtle, medium, strong
- Breakpoints: mobile (640px), tablet (1024px), desktop
Example: Complete token set
{
"color": {
"primary": {
"50": "#eff6ff",
"500": "#2563eb",
"900": "#1e3a8a"
},
"neutral": {
"50": "#f9fafb",
"500": "#6b7280",
"900": "#111827"
},
"semantic": {
"error": "#dc2626",
"success": "#16a34a",
"warning": "#f59e0b"
}
},
"spacing": {
"4": "4px",
"8": "8px",
"12": "12px",
"16": "16px",
"24": "24px",
"32": "32px"
},
"typography": {
"fontFamily": {
"sans": "Inter, system-ui, -apple-system, sans-serif",
"mono": "Fira Code, monospace"
},
"fontSize": {
"xs": "12px",
"sm": "14px",
"base": "16px",
"lg": "18px",
"xl": "20px",
"2xl": "24px"
},
"lineHeight": {
"tight": "1.25",
"normal": "1.5",
"relaxed": "1.75"
}
}
}
Step 3: Build Core Components
Start with the 10โ15 most frequently used components. You can add more later.
Essential components for most products:
| Component | Why It Matters | Variants |
|---|---|---|
| Button | CTAs everywhere | primary, secondary, ghost, disabled |
| Input | Forms, search | text, password, disabled, error |
| Select | Dropdowns | default, open, disabled |
| Checkbox | Multi-select | checked, unchecked, disabled |
| Radio | Single-select | selected, unselected |
| Card | Content grouping | default, elevated, clickable |
| Badge | Labels, status | default, primary, success, error |
| Modal | Dialogs, confirmations | small, medium, large |
| Alert | Notifications | info, success, warning, error |
| Form Field | Label + input + error | all input states |
Example: Building a reusable Button component
// Button.jsx
import React from 'react';
import styles from './Button.module.css';
export function Button({
children,
variant = 'primary',
size = 'md',
disabled = false,
isLoading = false,
onClick,
...props
}) {
const className = [
styles.button,
styles[`variant-${variant}`],
styles[`size-${size}`],
disabled && styles.disabled,
isLoading && styles.loading,
]
.filter(Boolean)
.join(' ');
return (
<button
className={className}
disabled={disabled || isLoading}
onClick={onClick}
aria-busy={isLoading}
{...props}
>
{isLoading ? <Spinner /> : children}
</button>
);
}
/* Button.module.css */
.button {
font-family: var(--font-family);
font-size: var(--font-size-base);
font-weight: 600;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
}
.variant-primary {
background-color: var(--color-primary);
color: white;
}
.variant-primary:hover:not(:disabled) {
background-color: #1d4ed8;
box-shadow: var(--shadow-md);
}
.variant-secondary {
background-color: var(--color-secondary);
color: white;
}
.size-sm {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: var(--font-size-sm);
}
.size-md {
padding: var(--spacing-sm) var(--spacing-md);
min-height: 40px;
}
.size-lg {
padding: var(--spacing-md) var(--spacing-lg);
min-height: 48px;
font-size: var(--font-size-lg);
}
.disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading {
color: transparent;
}
Step 4: Create Variants and States
Every component needs to handle multiple states. Document them clearly.
States checklist:
[ ] Default state
[ ] Hover state
[ ] Active/focus state
[ ] Disabled state
[ ] Loading state (if applicable)
[ ] Error state
[ ] Success state
Example: Input field with all states
// Input.jsx
export function Input({
value,
error,
disabled,
isLoading,
...props
}) {
const className = [
'input',
error && 'input-error',
disabled && 'input-disabled',
isLoading && 'input-loading',
]
.filter(Boolean)
.join(' ');
return (
<>
<input
className={className}
disabled={disabled || isLoading}
value={value}
{...props}
/>
{error && <span className="input-error-message">{error}</span>}
</>
);
}
.input {
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
font-size: var(--font-size-base);
transition: all 0.2s ease;
}
.input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.input-error {
border-color: var(--color-error);
}
.input-error:focus {
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
}
.input-disabled {
background-color: #f3f4f6;
color: #9ca3af;
cursor: not-allowed;
}
.input-error-message {
display: block;
color: var(--color-error);
font-size: var(--font-size-sm);
margin-top: var(--spacing-xs);
}
Step 5: Document & Create Examples
Good documentation means your team (or future you) can use components correctly.
What to document for each component:
- Purpose: One sentence on what the component does
- When to use: Recommended scenarios
- When NOT to use: Common mistakes
- Props/variants: What options does it have?
- Code example: Copy-paste ready
- Accessibility notes: Keyboard support, ARIA attributes
Example documentation:
## Button
### Purpose
Primary interaction element for user actions like submitting forms or navigating.
### When to Use
- Primary CTAs (e.g., "Sign Up", "Submit")
- Secondary actions (e.g., "Cancel", "Learn More")
- Ghost buttons for tertiary actions
### When NOT to Use
- Don't use for navigation; use links instead
- Don't use multiple primary buttons in one section
- Don't put long text in buttons (max 2 words)
### Variants
- **Primary:** Main action, brand color background
- **Secondary:** Supporting action, neutral background
- **Ghost:** Tertiary action, no background
### Example
\`\`\`jsx
<Button variant="primary" onClick={handleSubmit}>
Sign Up
</Button>
<Button variant="secondary" disabled>
Disabled
</Button>
\`\`\`
### Accessibility
- Keyboard accessible (Tab, Enter)
- Focus visible by default
- Disabled state prevents interaction
- ARIA labels for icon-only buttons
Tools for documentation:
- Storybook: storybook.js.org - Living component library
- Notion: Free wiki for smaller design systems
- GitHub Pages: Host markdown docs for free
- Figma: Built-in documentation in Figma files
Step 6: Handoff to Dev
Make implementation easy by providing clear specs and code.
What to provide:
-
Design file (Figma/Sketch)
- Well-organized with components in separate pages
- Naming convention:
Component / Variant / State - Tokens linked to actual values
-
Token export
- JSON for programmatic use
- CSS variables file
- TypeScript types (if applicable)
-
Component code
- Working examples in your stack (React, Vue, etc.)
- Storybook or similar documentation
- Unit tests for critical behavior
-
Implementation guide
- How to import components
- Common patterns (forms, modals, notifications)
- Troubleshooting guide
Example handoff checklist:
[ ] Figma components built and organized
[ ] Token JSON exported
[ ] CSS variables generated
[ ] React components coded and tested
[ ] Storybook stories written
[ ] Documentation complete
[ ] Accessibility audit passed
[ ] Developer README written
Tools & Resources
Design Tools
| Tool | Best For | Price |
|---|---|---|
| Figma | Design, prototyping, collaboration | Freeโ$25/month |
| Sketch | Mac-native design, components | $120/year |
| Adobe XD | Design, prototyping, collaboration | $10โ$60/month |
Component Documentation
| Tool | Best For | Price |
|---|---|---|
| Storybook | Interactive component documentation | Free |
| Zeroheight | Design system documentation | Freeโ$100/month |
| Supernova | Token management and docs | Paid |
Token Management
| Tool | Best For | Price |
|---|---|---|
| Style Dictionary | Token conversion (JSON โ CSS/JS/etc.) | Free |
| Tokens Studio | Token management in Figma | Freeโ$120/year |
| FigmaTokens | Token sync from Figma to code | Paid |
UI Component Libraries (Start Here)
| Library | Best For | Tech |
|---|---|---|
| Tailwind CSS | Rapid UI development, utility-first CSS | CSS framework |
| Chakra UI | Accessible component library | React |
| shadcn/ui | Copy-paste components, customizable | React |
| Radix UI | Headless components, flexible styling | React |
| Material Design | Comprehensive design system | Multi-platform |
Accessibility Resources
- WebAIM - Comprehensive accessibility guides
- WCAG 2.1 - Web Content Accessibility Guidelines
- A11y Project - Practical accessibility tips
- axe DevTools - Browser accessibility testing
Lightweight Design System Template (Starter)
Here’s a minimal template to get started this week:
1. Tokens File (tokens.json)
{
"color": {
"primary": "#2563eb",
"secondary": "#64748b",
"success": "#16a34a",
"warning": "#f59e0b",
"error": "#dc2626",
"background": "#ffffff",
"surface": "#f9fafb",
"text": "#1e293b",
"text-subtle": "#64748b",
"border": "#e2e8f0"
},
"spacing": {
"2": "2px",
"4": "4px",
"8": "8px",
"12": "12px",
"16": "16px",
"24": "24px",
"32": "32px",
"48": "48px"
},
"typography": {
"fontSize": {
"xs": "12px",
"sm": "14px",
"base": "16px",
"lg": "18px",
"xl": "20px",
"2xl": "24px"
},
"fontWeight": {
"normal": 400,
"medium": 500,
"semibold": 600,
"bold": 700
}
},
"radius": {
"sm": "4px",
"md": "6px",
"lg": "8px"
}
}
2. Core Components
components/
โโโ Button/
โ โโโ Button.jsx
โ โโโ Button.css
โ โโโ Button.stories.jsx
โโโ Input/
โ โโโ Input.jsx
โ โโโ Input.css
โ โโโ Input.stories.jsx
โโโ Card/
โ โโโ Card.jsx
โ โโโ Card.css
โ โโโ Card.stories.jsx
โโโ Modal/
โ โโโ Modal.jsx
โ โโโ Modal.css
โโโ Alert/
โโโ Alert.jsx
โโโ Alert.css
3. CSS Variables
/* styles/tokens.css */
:root {
--color-primary: #2563eb;
--color-secondary: #64748b;
--color-success: #16a34a;
--color-error: #dc2626;
--color-background: #ffffff;
--color-text: #1e293b;
--color-border: #e2e8f0;
--spacing-2: 2px;
--spacing-4: 4px;
--spacing-8: 8px;
--spacing-16: 16px;
--spacing-24: 24px;
--font-size-base: 16px;
--font-size-lg: 18px;
--radius-md: 6px;
}
4. Component Library (Storybook)
npm install -D storybook @storybook/react
npx storybook init
Then create .stories.jsx files for each component with live examples.
Maintenance Plan
Weekly
- Use components in new features
- Note any missing variants or patterns
- Fix bugs reported by team
Monthly
- Review new components added (should be < 3)
- Check for design debt (colors not matching tokens, etc.)
- Update documentation if processes changed
Quarterly
- Audit the design system for drift and inconsistencies
- Add new components only if the pattern repeats 3+ times
- Get feedback from team on usability
When to Expand
- Add a new component when you’ve built it custom 3+ times
- Add tokens when you find yourself repeating values
- Add documentation when you explain the same thing twice
When to Simplify
- Remove unused components (< 2 uses in 6 months)
- Consolidate variants (e.g., “primary” and “main” should be one)
- Simplify documentation (if it’s too long, break it into smaller pieces)
Real-World Examples
Example 1: Building a Form Field Component
// FormField.jsx - Combines label, input, error, and help text
export function FormField({
label,
error,
helpText,
required,
...inputProps
}) {
return (
<div className="form-field">
{label && (
<label className="form-label">
{label}
{required && <span className="required">*</span>}
</label>
)}
<Input {...inputProps} />
{error && <span className="error-message">{error}</span>}
{helpText && <span className="help-text">{helpText}</span>}
</div>
);
}
// Usage
<FormField
label="Email"
type="email"
required
error="Invalid email"
helpText="We'll never share your email"
/>
Example 2: Dark Mode with Tokens
/* Light mode (default) */
:root {
--color-background: #ffffff;
--color-text: #1e293b;
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
:root {
--color-background: #0f172a;
--color-text: #f1f5f9;
}
}
/* User-selected dark mode */
html[data-theme="dark"] {
--color-background: #0f172a;
--color-text: #f1f5f9;
}
Example 3: Responsive Design with Tokens
/* Mobile first */
:root {
--spacing-section: var(--spacing-16);
--font-size-heading: var(--font-size-xl);
}
/* Tablet */
@media (min-width: 768px) {
:root {
--spacing-section: var(--spacing-24);
--font-size-heading: var(--font-size-2xl);
}
}
/* Desktop */
@media (min-width: 1024px) {
:root {
--spacing-section: var(--spacing-32);
}
}
Common Mistakes to Avoid
โ Building Too Much Too Soon
- Problem: Spending weeks building components you don’t use yet
- Solution: Audit your UI first, build only top 10 components
โ Over-Engineering Variants
- Problem: 50 button variants when 3 would suffice
- Solution: Start with primary/secondary/ghost, add more only if needed
โ Inconsistent Naming
- Problem: “Button.primary” vs “Button.main” vs “Button.cta”
- Solution: Define naming conventions upfront (variant-based, not semantic)
โ Poor Documentation
- Problem: Components exist but no one knows how to use them
- Solution: Storybook with copy-paste examples for every component
โ Ignoring Accessibility
- Problem: Components that don’t support keyboard navigation or screen readers
- Solution: Test with accessibility tools from day one (axe, Lighthouse)
โ Disconnected Design and Code
- Problem: Designers design, developers implement differently
- Solution: Keep Figma and code in sync (use token sync tools)
Final Thoughts
Building a full design system is not necessary for every indie hacker. Instead, focus on a small set of reusable components and tokens that reduce repetition and speed up shipping. Over time you can expand the system as your product and team grow.
Remember:
- Start small (10 components, basic tokens)
- Automate boring tasks (token export, component generation)
- Document heavily (one-paragraph per component)
- Iterate based on use (remove unused components, add new patterns)
The goal isn’t a perfect design systemโit’s a system that makes shipping faster.
Resources & Next Steps
Getting Started
- Figma Community - Free design system templates to customize
- UI Component Templates - Ready-made components
Learning More
- “Design Systems Handbook” - Free ebook from DesignBetter.co
- “A List Apart: Design Systems” - In-depth articles
- Carbon Design System - Enterprise design system case study
- Stripe Design Patterns - Real-world approach to scaling design
Action Items This Week
- Screenshot your top 10 UI components
- Create a tokens.json file with your colors, spacing, and typography
- Build a Button component with 3 variants (primary, secondary, ghost)
- Set up Storybook or documentation site
- Share with your team and get feedback
Comments