Tutorial

Building a Design System Without a Designer: A Developer's Guide

Complete guide for solo developers and small teams to build a production-ready design system using systematic approaches, AI tools, and practical constraints.

FramingUI Team11 min read

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:

  1. Color palette — 5-8 semantic colors (brand, text, backgrounds, borders, status)
  2. Spacing scale — 6-8 values (4px, 8px, 16px, 24px, 32px, 48px, 64px)
  3. Typography — 4-6 font sizes with consistent line heights
  4. Border radius — 3-4 values (none, small, medium, large)
  5. 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:

  1. Generate several palettes
  2. Pick the one that feels right for your product category (finance = blue, creative = purple, etc.)
  3. Extract 2-3 core colors
  4. 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] or spacing[3]
  • Cards, panels: padding: spacing[4] or spacing[6]
  • Page margins: spacing[8] or spacing[12]
  • Gaps in layouts: spacing[4] or spacing[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:

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-xl or text-2xl
  • Page titles: text-3xl or text-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

  1. Pick a color palette — Steal from Tailwind or a product you admire
  2. Define spacing scale — Use 4px or 8px base
  3. Choose typography — Start with Inter for everything
  4. Build 5 primitives — Button, Input, Card, Container, Stack
  5. Document tokens — Create DESIGN_SYSTEM.md
  6. Enforce with tooling — Constrain Tailwind config, add ESLint rules
  7. 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.

Ready to build with FramingUI?

Build consistent UI with AI-ready design tokens. No more hallucinated colors or spacing.

Try FramingUI
Share

Related Posts