Building a Design System with Tailwind CSS
Source: Dev.to
Overview
“Tailwind is utility classes. Does it work for design systems?”
Answer: Yes. Actually, it works great.
The core of a design system is consistency—colors, spacing, typography, and other tokens following defined rules. Tailwind enforces that consistency:
p-4is always 16 px.text-smis always 14 px.
No developer variance.
Basic Structure
Design Tokens (CSS Variables)
↓
Tailwind Config (tailwind.config)
↓
Utility Classes (bg-primary, text-sm)
↓
Components
1. Color Tokens
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6', // base
600: '#2563eb', // hover
700: '#1d4ed8', // active
},
gray: {
50: '#f9fafb',
100: '#f3f4f6',
500: '#6b7280',
900: '#111827',
},
},
},
},
};
Now you can use bg-primary-500, text-gray-900.
CSS Variables Are Better
/* globals.css */
:root {
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
}
.dark {
--color-primary-500: #60a5fa;
--color-primary-600: #3b82f6;
}
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
primary: {
500: 'var(--color-primary-500)',
600: 'var(--color-primary-600)',
},
},
},
},
};
Dark‑mode switching now happens automatically.
2. Typography
// tailwind.config.ts
export default {
theme: {
extend: {
fontSize: {
'display-lg': ['48px', { lineHeight: '1.2', fontWeight: '700' }],
'display-md': ['36px', { lineHeight: '1.2', fontWeight: '700' }],
'heading-lg': ['24px', { lineHeight: '1.4', fontWeight: '600' }],
'heading-md': ['20px', { lineHeight: '1.4', fontWeight: '600' }],
'body-lg': ['18px', { lineHeight: '1.6' }],
'body-md': ['16px', { lineHeight: '1.6' }],
'body-sm': ['14px', { lineHeight: '1.6' }],
},
},
},
};
// Example usage in a component
<h1 className="text-display-lg">Large Title</h1>
<h2 className="text-display-md">Medium Title</h2>
<p className="text-body-md">Body text</p>
Responsive Typography
CSS variables make responsiveness easy:
:root {
--fs-display-lg: 36px;
}
@media (min-width: 1024px) {
:root {
--fs-display-lg: 48px;
}
}
// tailwind.config.ts
export default {
theme: {
extend: {
fontSize: {
'display-lg': ['var(--fs-display-lg)', { lineHeight: '1.2' }],
},
},
},
};
Now a single text-display-lg class handles both mobile and desktop sizes.
3. Spacing
Tailwind’s default spacing works for most cases, but you can define a custom scale:
// tailwind.config.ts
export default {
theme: {
extend: {
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
'2xl': '48px',
'3xl': '64px',
},
},
},
};
{/* padding: 16px, margin‑bottom: 24px */}
In practice, Tailwind’s built‑in spacing (p-4, mb-6, etc.)—an 8 px grid—is often sufficient.
4. Building Components
With tokens set up, you can create reusable components.
// Button.tsx
const variants = {
primary: 'bg-primary-500 text-white hover:bg-primary-600',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
outline: 'border border-primary-500 text-primary-500 hover:bg-primary-50',
};
const sizes = {
sm: 'h-8 px-3 text-body-sm',
md: 'h-10 px-4 text-body-md',
lg: 'h-12 px-6 text-body-lg',
};
export function Button({ variant = 'primary', size = 'md', ...props }) {
return (
// component implementation here
);
}
Type‑Safe with cva
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md font-medium',
{
variants: {
variant: {
primary: 'bg-primary-500 text-white hover:bg-primary-600',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
},
size: {
sm: 'h-8 px-3 text-body-sm',
md: 'h-10 px-4 text-body-md',
lg: 'h-12 px-6 text-body-lg',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
type ButtonProps = VariantProps<typeof buttonVariants>;
// { variant?: 'primary' | 'secondary'; size?: 'sm' | 'md' | 'lg' }
Autocomplete and type‑checking are now built‑in.
5. Distribute as a Preset
Create a reusable preset for multiple projects or a team:
// my-preset.ts
const myPreset = {
theme: {
extend: {
colors: { /* … */ },
fontSize: { /* … */ },
},
},
};
export default myPreset;
// tailwind.config.ts
import myPreset from './my-preset';
export default {
presets: [myPreset],
// project‑specific additions go here
};
Design System Checklist
- Color tokens (primary, secondary, gray, danger, success)
- Typography scale (display, heading, body, label)
- Spacing system (8 px grid recommended)
- Border radius
- Shadows
- Base components (Button, Input, Card, etc.)
For Government Projects
When working on Korean government projects you need to follow KRDS (Korean Design System).
All color codes, font sizes, spacing, etc., are strictly defined, and setting them up manually can take a full day.
HANUI – KRDS preset for TailwindCSS
HANUI provides KRDS tokens pre‑applied as a Tailwind preset, so you can get a compliant UI up and running instantly.
Tailwind configuration
// tailwind.config.ts
import hanuiPreset from '@hanui/react/tailwind.preset';
export default {
presets: [hanuiPreset],
};
Using KRDS classes in your components
// Example component
<div className="text-krds-body">
KRDS styled text
</div>
<button className="krds-button">
KRDS button
</button>
What You Get
- Colors – fully KRDS‑compliant palette
- Typography – correct font families, sizes, and line‑heights
- Responsive design – built‑in breakpoints that follow KRDS guidelines
- Tech stack – TailwindCSS, React, CSS variables, CVA (class‑variance‑authority)
All of the above lets you focus on building features instead of painstakingly recreating the design system.