How to Set Up a Design System in 10 Minutes (Without a Designer)
You don't need Figma, a design team, or a 200-page style guide. Here's how to set up a functional design system with Tailwind and CSS variables in under 10 minutes.
You've been staring at a blank Figma canvas for 45 minutes trying to decide between #2563EB and #3B82F6 for your primary button. Your database schema is done. Your auth flow works perfectly. But you are paralyzed by hex codes, font pairings, and a sudden, irrational urge to install Storybook. Stop. You are an indie hacker, not the UX lead at Airbnb.
Developers read engineering blogs from Spotify or Linear and assume they need a 200-page style guide to launch a SaaS. You don't. A design system for a solo developer or a small technical team is just a mechanism to prevent you from accidentally using 14 different shades of gray across 3 pages.
The "Enterprise Design System" Trap
Setting up a "proper" design system with Storybook, Figma token syncing, and automated visual regression testing takes roughly 15 hours. That is 15 hours you are not shipping features.
When you build a design system before you have product-market fit, you are procrastinating. You end up maintaining a component library for an audience of one. The goal is not to build a comprehensive UI kit. The goal is to establish strict constraints so you never have to make a design decision while writing React code.
You need exactly four things to achieve this: colors, typography, spacing, and base components.
| Feature | Enterprise Design System | Minimum Viable Design System (MVDS) | Winner for Indie Devs |
|---|---|---|---|
| Source of Truth | Figma + Design Tokens Sync | tailwind.config.ts + globals.css | MVDS. Skips the Figma tax. |
| Documentation | Storybook with MDX | CLAUDE.md + a single /components route | MVDS. Zero maintenance. |
| Color Scale | 12 custom scales (0-1000) | 1 Primary, 1 Destructive, 1 Neutral | MVDS. Prevents decision fatigue. |
| Component Variants | 36 per component | Primary, Secondary, Outline | MVDS. Covers 99% of use cases. |
Step 1: The CSS Variable Foundation
Hardcoding hex values in your Tailwind config breaks dark mode and makes theming impossible. You must use CSS variables. Specifically, you must use HSL values without the hsl() wrapper.
Why? Because Tailwind's <alpha-value> modifier requires raw numbers to inject opacity. If you define --primary: #2563EB, writing bg-primary/50 fails.
Paste this into your globals.css. It defines your core semantic tokens, a single 7-color gray scale (50-950), and your border radius.
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
Generating the math for these HSL scales manually is tedious. You can use a tool like UI Colors, tweak the bezier curves, export the CSS, and manually copy it over. Or you can use OneMinuteBranding. You pay $49 one-time, and in 60 seconds you get your `tailwind.config.ts`, your CSS variables mapped from 50 to 950, a production-ready logo, and a `CLAUDE.md` file. The `CLAUDE.md` file is critical—it feeds your exact design tokens directly into Cursor or Claude so your AI stops hallucinating `bg-blue-500` when you have a defined `--primary` token.
## Step 2: The Opinionated Tailwind Config
Your `tailwind.config.ts` is the enforcement mechanism for your design system. By default, Tailwind gives you 32 different colors. This is a vulnerability. If you leave the default palette open, you will eventually use `text-slate-500` in one file and `text-gray-500` in another.
Map your CSS variables into Tailwind's theme extension.
```typescript
import type { Config } from "tailwindcss"
const config = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
} satisfies Config
export default configNotice the semantic naming: bg-primary, not bg-blue-600. When you pivot your SaaS from a developer tool (blue) to an enterprise HR platform (purple), you change exactly two lines in globals.css. If you use literal color names in your classes, you will spend 4 hours running regex find-and-replace across your codebase.
Step 3: Typography and Spacing Constraints
Font pairings are a massive time sink. Pick one sans-serif font for your UI (Inter, Geist, or system-ui) and one monospace font for code blocks or data tables (Geist Mono, JetBrains Mono). Add them to your root layout and inject them via CSS variables.
Spacing is already solved by Tailwind's mathematical scale. The default 4-point grid (p-4 = 1rem = 16px) is flawless. Your only job is to restrict your usage.
Establish a mental rule right now:
gap-2for elements inside a component (icon next to text).gap-4for components inside a card.gap-8for sections on a page.gap-16for distinct landing page sections.
If you find yourself typing mt-7 or pb-[18px], you are breaking the system. Delete it and use the closest scale value.
Step 4: Base Components with CVA
Your design tokens are useless if you manually type bg-primary text-primary-foreground hover:bg-primary/90 px-4 py-2 rounded-md every time you need a button.
You need exactly four base components to build 80% of any SaaS: Button, Input, Card, and Dialog.
Use class-variance-authority (CVA) to build these. CVA allows you to map your Tailwind utility classes to semantic React props.
npm install class-variance-authority clsx tailwind-mergeHere is your universal Button component. It handles the primary action, secondary action, destructive action, and ghost variants. It uses tailwind-merge to resolve class conflicts if you need to pass a custom className for spacing.
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }You now have a strict API for your UI. <Button variant="destructive">Delete</Button>. You never have to think about hover states or focus rings again.
When to Actually Build a "Real" Design System
Do not upgrade from this setup until you hit at least one of these three metrics:
- You hire a dedicated designer and a 3rd frontend engineer. When communication overhead exceeds the time it takes to maintain Storybook, you need a dedicated system.
- You are building cross-platform. If you have a React web app and a native Swift iOS app, Tailwind configs don't work. You need a tool like Style Dictionary to compile JSON design tokens into CSS, Swift, and Android XML.
- You are white-labeling your SaaS. If you sell B2B software where every client gets their own branding, a simple
globals.cssfile becomes a bottleneck. You need a database-driven token system.
If you don't meet these criteria, your tailwind.config.ts is your design system.
FAQ
Should I use styled-components or Emotion instead of Tailwind?
No. Tailwind won the framework war. CSS-in-JS libraries add runtime performance overhead, break React Server Components by default, and have shrinking ecosystems. Tailwind compiles to static CSS and works universally.
How do I handle dark mode without doubling my code?
You already did it in Step 1. Because your Tailwind config maps bg-background to hsl(var(--background)), you simply change the HSL values inside the .dark class in your CSS. Add next-themes to toggle the .dark class on the <html> tag. Zero changes required in your React components.
Do I need to design my screens in Figma first?
Not until you have a designer. Designing in Figma and then translating it to code means you are doing the work twice. With a strict token system and CVA components, it is faster to design directly in the browser. Build your UI with code.
My AI assistant keeps suggesting wrong Tailwind classes. How do I fix this?
LLMs are trained on millions of repositories using default Tailwind colors (bg-blue-500). They don't know you use semantic tokens (bg-primary). Create a CLAUDE.md or .cursorrules file in your root directory. Write a strict prompt: "Never use literal color names. Only use bg-primary, bg-secondary, bg-destructive, and bg-background. Border radius is always rounded-md."
Copy the globals.css and tailwind.config.ts snippets from Step 1 and 2 right now. Paste them into your project. Build your Button component. Close your color picker. Start writing your backend logic.
Vibe coder & Indie Hacker. Building tools to help devs ship faster. Creator of OneMinuteBranding.
Ready to create your brand?
Generate a complete brand system with Tailwind config in 60 seconds.
Generate your brand