Why Every Project Needs a Design System (Even Small Ones)
We used to think design systems were a "big company" problem. After all, if you're a team of 6 building a 30-page site, do you really need a formal design system?
The answer, we've learned, is yes — but a lightweight one.
Without a system, designers make a button and developers implement it differently. Colors drift. Spacing is inconsistent. The site looks cohesive in Figma and chaotic in the browser.
A design system eliminates that drift. And with Tailwind CSS, it's surprisingly lightweight to maintain.
The Three Layers of Our System
1. Design Tokens (tailwind.config.ts)
Everything starts here. Colors, spacing, typography, shadows — all defined as first-class Tailwind tokens:
// tailwind.config.ts
const config: Config = {
theme: {
extend: {
colors: {
primary: {
DEFAULT: "#FFDB08",
50: "#FFFDE0",
// ...full scale
},
neutral: {
// Custom gray scale
},
},
fontFamily: {
sans: ["var(--font-geist-sans)", "system-ui"],
mono: ["var(--font-geist-mono)", "monospace"],
},
spacing: {
// We extend with semantic spacing
"section-sm": "4rem",
"section-md": "6rem",
"section-lg": "8rem",
},
},
},
};
The key insight: every design decision in Figma must have a corresponding token in Tailwind. If the designer uses a color, it should be in the config. No raw hex codes in components.
2. Base Component Styles
We use Tailwind's @layer components for repeated patterns:
@layer components {
.btn {
@apply inline-flex items-center justify-center gap-2 font-semibold rounded-xl transition-all duration-200;
}
.btn-primary {
@apply btn px-6 py-3 bg-gray-900 text-white hover:bg-primary hover:text-black;
}
.btn-secondary {
@apply btn px-6 py-3 border border-gray-200 text-gray-700 hover:border-gray-300 hover:shadow-sm;
}
.card {
@apply rounded-2xl border border-gray-100 bg-white transition-all duration-300;
}
.card-hover {
@apply card hover:border-primary/40 hover:shadow-lg;
}
}
3. React Components
Our component library lives in /components/ui/ and follows a consistent API:
// components/ui/Button.tsx
interface ButtonProps {
variant?: "primary" | "secondary" | "ghost";
size?: "sm" | "md" | "lg";
children: React.ReactNode;
className?: string;
}
export function Button({
variant = "primary",
size = "md",
children,
className,
...props
}: ButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
className={cn(
"btn",
variants[variant],
sizes[size],
className
)}
{...props}
>
{children}
</button>
);
}
The Figma ↔ Tailwind Handoff
The handoff between design and development is where most inconsistencies creep in. Our process:
- Design in Figma using components that map 1:1 to our Tailwind tokens
- Name everything consistently — if it's
primary-500in Tailwind, it'sprimary/500in Figma - Document in Figma — every component has a playground with all variants
- Review together — designer and developer review the first implementation side-by-side
This last step sounds obvious but is routinely skipped. It's worth the 30 minutes.
Common Pitfalls
1. Too many variants too soon
Start with the minimum. Add variants only when you have a real use case. A Button component with 12 variants is almost never justified.
2. Ignoring the dark mode consideration early
Even if you're not shipping dark mode, design your tokens semantically from day one. bg-surface is better than bg-white if there's any chance of a dark theme later.
3. One-offs accumulate
The most dangerous phrase in design system work is "just this once." Every exception becomes a precedent. Push back on one-offs aggressively — either the component should exist or the design should change.
The Payoff
After a month of working within a defined system, we can implement new pages in Figma and code them in a fraction of the time. The components are there, the tokens are set, and everyone speaks the same language.
The upfront investment is 1–2 weeks. The payoff compounds forever.
Want to talk design systems or UI architecture? Get in touch.