Building Design Systems with Tailwind CSS
Tailwind is powerful for component-level styling, but large teams need structure. Design systems with Tailwind require layers, tokens, and clear conventions.
Tailwind Layers
Tailwind's cascade organizes CSS into three layers:
@layer base { } /* Typography, resets */
@layer components { } /* Reusable components */
@layer utilities { } /* Single-purpose utilities */
Base layer: Global typography, link styles, form resets.
@layer base {
body {
@apply font-sans text-gray-900;
}
a {
@apply underline hover:no-underline;
}
}
Components layer: Multi-class patterns like buttons, cards, inputs.
@layer components {
.btn-primary {
@apply px-4 py-2 bg-blue-500 text-white rounded font-semibold hover:bg-blue-600;
}
.card {
@apply bg-white rounded-lg shadow p-6;
}
}
Utilities layer: Single-purpose classes (Tailwind's default). Only override here if you need custom utilities.
Token Definition
Design tokens are the source of truth for colors, spacing, typography. Define in Tailwind config:
// tailwind.config.js
module.exports = {
theme: {
colors: {
primary: '#2563eb',
secondary: '#64748b',
success: '#16a34a',
error: '#dc2626',
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
},
fontSize: {
sm: ['0.875rem', '1.25rem'],
base: ['1rem', '1.5rem'],
lg: ['1.125rem', '1.75rem'],
},
},
}
Use tokens consistently: bg-primary instead of bg-blue-500. If branding changes, update the config once; all usages update automatically.
Component Library Structure
Build a React component library on top of Tailwind:
components/
buttons/
Button.tsx
IconButton.tsx
forms/
Input.tsx
Select.tsx
Checkbox.tsx
layout/
Card.tsx
Modal.tsx
index.ts
Each component encapsulates Tailwind classes:
// components/buttons/Button.tsx
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'outline'
size?: 'sm' | 'md' | 'lg'
children: React.ReactNode
}
export function Button({ variant = 'primary', size = 'md', children }: ButtonProps) {
const baseClasses = 'font-semibold rounded transition-colors'
const variantClasses = {
primary: 'bg-primary text-white hover:bg-primary-dark',
secondary: 'bg-secondary text-white hover:bg-secondary-dark',
outline: 'border-2 border-primary text-primary hover:bg-primary hover:text-white',
}
const sizeClasses = {
sm: 'px-2 py-1 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
}
return (
<button className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}>
{children}
</button>
)
}
Responsive Design Patterns
Tailwind's responsive prefixes scale gracefully:
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
{/* 1 column on mobile, 2 on tablet, 3 on desktop */}
</div>
Define breakpoints in config if you need non-standard sizes:
theme: {
screens: {
sm: '480px',
md: '768px',
lg: '1024px',
xl: '1280px',
},
}
Dark Mode
Enable dark mode in config:
module.exports = {
darkMode: 'class', // Uses class strategy: <html class="dark">
// or 'media' for system preference
}
Then use dark: prefix:
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
Adapts to dark mode
</div>
Plugin Development
For reusable utilities across projects, build Tailwind plugins:
// plugins/custom-utilities.js
module.exports = function({ addUtilities }) {
addUtilities({
'.text-gradient': {
backgroundImage: 'linear-gradient(to right, var(--tw-gradient-stops))',
'-webkit-background-clip': 'text',
'-webkit-text-fill-color': 'transparent',
},
})
}
// tailwind.config.js
plugins: [require('./plugins/custom-utilities')],
Best Practices
- Don't over-engineer: Simple utility classes are fine. Not everything needs a component.
- Name components clearly:
.card-elevatednot.box. Intent matters. - Test themes: Dark mode, high contrast, accessibility. Use tools like axe DevTools.
- Document tokens: Create a Storybook or component explorer showing all buttons, colors, spacing.
- Audit regularly: Unused classes add bundle size. Use Tailwind's purge config to remove them.
Frequently asked questions
Should I create components for every Tailwind utility combination?
No. Components should represent reusable patterns (buttons, cards). Simple utilities like flex, gap, and text-colors can stay in HTML.
How do I manage Tailwind themes for multiple brands?
Define brands in config, generate theme files, or use CSS custom properties. Switch themes via class or data attribute on root element.
What's the performance impact of Tailwind?
Minimal. Tailwind purges unused classes in production. Bundle size is typically 10-40KB gzipped depending on feature usage.