Skip to main content
โšก Calmops

Indie Hacker Design System: Build Reusable UI for Faster Delivery

Create a lightweight design system that scales your product, improves consistency, and simplifies your development workflow

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:

  1. Screenshot every unique component in your product
  2. Categorize: buttons, inputs, cards, modals, notifications, etc.
  3. Identify patterns: What components repeat? What variations exist?
  4. 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:

  1. 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
  2. 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
  3. 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)
  4. 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:

  1. Purpose: One sentence on what the component does
  2. When to use: Recommended scenarios
  3. When NOT to use: Common mistakes
  4. Props/variants: What options does it have?
  5. Code example: Copy-paste ready
  6. 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:

  1. Design file (Figma/Sketch)

    • Well-organized with components in separate pages
    • Naming convention: Component / Variant / State
    • Tokens linked to actual values
  2. Token export

    • JSON for programmatic use
    • CSS variables file
    • TypeScript types (if applicable)
  3. Component code

    • Working examples in your stack (React, Vue, etc.)
    • Storybook or similar documentation
    • Unit tests for critical behavior
  4. 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


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

Learning More

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