Tailwind CSS Component Patterns: Building Reusable UI
Building scalable applications requires well-structured, reusable components. Tailwind CSS provides the flexibility to create consistent design patterns while maintaining the utility-first approach. Let's explore advanced techniques for building maintainable component libraries.
Component Composition Patterns
Base Component Pattern
Create flexible base components that can be extended:
// Base Button Component
function Button({
variant = 'primary',
size = 'md',
children,
className = '',
...props
}) {
const baseClasses = 'inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none'
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
outline: 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 focus:ring-blue-500',
ghost: 'text-gray-700 hover:bg-gray-100 focus:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
}
const sizes = {
sm: 'px-3 py-1.5 text-sm rounded-md',
md: 'px-4 py-2 text-sm rounded-md',
lg: 'px-6 py-3 text-base rounded-md',
xl: 'px-8 py-4 text-lg rounded-lg',
}
const classes = `${baseClasses} ${variants[variant]} ${sizes[size]} ${className}`
return (
<button className={classes} {...props}>
{children}
</button>
)
}
// Usage Examples
function ButtonExamples() {
return (
<div className="space-y-4 p-8">
<div className="flex gap-4">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="danger">Danger</Button>
</div>
<div className="flex items-center gap-4">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra Large</Button>
</div>
</div>
)
}
Card Component System
Build a flexible card system with multiple variants:
// Card Components
function Card({ children, className = '', ...props }) {
return (
<div
className={`bg-white rounded-lg border border-gray-200 shadow-sm ${className}`}
{...props}
>
{children}
</div>
)
}
function CardHeader({ children, className = '' }) {
return (
<div className={`px-6 py-4 border-b border-gray-200 ${className}`}>
{children}
</div>
)
}
function CardContent({ children, className = '' }) {
return (
<div className={`px-6 py-4 ${className}`}>
{children}
</div>
)
}
function CardFooter({ children, className = '' }) {
return (
<div className={`px-6 py-4 border-t border-gray-200 bg-gray-50 rounded-b-lg ${className}`}>
{children}
</div>
)
}
// Usage
function ProductCard({ product }) {
return (
<Card className="hover:shadow-md transition-shadow">
<CardHeader>
<img
src={product.image}
alt={product.name}
className="w-full h-48 object-cover rounded-t-lg"
/>
</CardHeader>
<CardContent>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{product.name}
</h3>
<p className="text-gray-600 text-sm mb-4">
{product.description}
</p>
<div className="flex items-center justify-between">
<span className="text-2xl font-bold text-green-600">
${product.price}
</span>
<span className="text-sm text-gray-500">
In stock: {product.stock}
</span>
</div>
</CardContent>
<CardFooter>
<div className="flex gap-2">
<Button variant="primary" className="flex-1">
Add to Cart
</Button>
<Button variant="outline">
<HeartIcon className="w-4 h-4" />
</Button>
</div>
</CardFooter>
</Card>
)
}
Form Component Patterns
Input Component with Variants
function Input({
label,
error,
helpText,
variant = 'default',
className = '',
...props
}) {
const baseClasses = 'block w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-0 transition-colors'
const variants = {
default: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
error: 'border-red-300 focus:border-red-500 focus:ring-red-500',
success: 'border-green-300 focus:border-green-500 focus:ring-green-500',
}
const inputVariant = error ? 'error' : variant
const inputClasses = `${baseClasses} ${variants[inputVariant]} ${className}`
return (
<div className="space-y-1">
{label && (
<label className="block text-sm font-medium text-gray-700">
{label}
</label>
)}
<input className={inputClasses} {...props} />
{error && (
<p className="text-sm text-red-600 flex items-center gap-1">
<ExclamationCircleIcon className="w-4 h-4" />
{error}
</p>
)}
{helpText && !error && (
<p className="text-sm text-gray-500">{helpText}</p>
)}
</div>
)
}
// Select Component
function Select({
label,
error,
options,
placeholder = 'Select an option',
className = '',
...props
}) {
const baseClasses = 'block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500'
const errorClasses = error ? 'border-red-300 focus:border-red-500 focus:ring-red-500' : ''
return (
<div className="space-y-1">
{label && (
<label className="block text-sm font-medium text-gray-700">
{label}
</label>
)}
<select
className={`${baseClasses} ${errorClasses} ${className}`}
{...props}
>
<option value="">{placeholder}</option>
{options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
{error && (
<p className="text-sm text-red-600">{error}</p>
)}
</div>
)
}
Layout Patterns
Container and Grid System
// Container Component
function Container({
size = 'default',
children,
className = ''
}) {
const sizes = {
sm: 'max-w-2xl',
default: 'max-w-4xl',
lg: 'max-w-6xl',
xl: 'max-w-7xl',
full: 'max-w-full',
}
return (
<div className={`mx-auto px-4 sm:px-6 lg:px-8 ${sizes[size]} ${className}`}>
{children}
</div>
)
}
// Grid Component
function Grid({
cols = 1,
gap = 4,
children,
className = ''
}) {
const gridCols = {
1: 'grid-cols-1',
2: 'grid-cols-1 md:grid-cols-2',
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
6: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6',
}
const gaps = {
2: 'gap-2',
4: 'gap-4',
6: 'gap-6',
8: 'gap-8',
}
return (
<div className={`grid ${gridCols[cols]} ${gaps[gap]} ${className}`}>
{children}
</div>
)
}
// Stack Component for vertical layouts
function Stack({
spacing = 4,
children,
className = ''
}) {
const spacings = {
2: 'space-y-2',
4: 'space-y-4',
6: 'space-y-6',
8: 'space-y-8',
}
return (
<div className={`${spacings[spacing]} ${className}`}>
{children}
</div>
)
}
Navigation Patterns
Breadcrumb Component
function Breadcrumb({ items, className = '' }) {
return (
<nav className={`flex ${className}`} aria-label="Breadcrumb">
<ol className="flex items-center space-x-1 md:space-x-3">
{items.map((item, index) => (
<li key={index} className="flex items-center">
{index > 0 && (
<ChevronRightIcon className="w-4 h-4 text-gray-400 mx-1" />
)}
{item.href ? (
<Link
href={item.href}
className="text-sm font-medium text-gray-500 hover:text-gray-700 transition-colors"
>
{item.label}
</Link>
) : (
<span className="text-sm font-medium text-gray-900">
{item.label}
</span>
)}
</li>
))}
</ol>
</nav>
)
}
// Tabs Component
function Tabs({ items, activeTab, onTabChange, className = '' }) {
return (
<div className={className}>
<nav className="flex space-x-8" aria-label="Tabs">
{items.map((item) => (
<button
key={item.id}
onClick={() => onTabChange(item.id)}
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === item.id
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{item.label}
</button>
))}
</nav>
</div>
)
}
Data Display Patterns
Table Component
function Table({ columns, data, className = '' }) {
return (
<div className={`overflow-x-auto ${className}`}>
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
{columns.map((column) => (
<th
key={column.key}
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
{column.label}
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{data.map((row, index) => (
<tr key={index} className="hover:bg-gray-50">
{columns.map((column) => (
<td
key={column.key}
className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"
>
{column.render ? column.render(row[column.key], row) : row[column.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}
// Badge Component
function Badge({
children,
variant = 'default',
size = 'md',
className = ''
}) {
const baseClasses = 'inline-flex items-center font-medium rounded-full'
const variants = {
default: 'bg-gray-100 text-gray-800',
primary: 'bg-blue-100 text-blue-800',
success: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
danger: 'bg-red-100 text-red-800',
}
const sizes = {
sm: 'px-2.5 py-0.5 text-xs',
md: 'px-3 py-0.5 text-sm',
lg: 'px-4 py-1 text-base',
}
const classes = `${baseClasses} ${variants[variant]} ${sizes[size]} ${className}`
return (
<span className={classes}>
{children}
</span>
)
}
Modal and Overlay Patterns
Modal Component
import { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react'
function Modal({
isOpen,
onClose,
title,
children,
size = 'md',
className = ''
}) {
const sizes = {
sm: 'max-w-md',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
}
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className={`w-full ${sizes[size]} transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all ${className}`}>
{title && (
<Dialog.Title className="text-lg font-medium leading-6 text-gray-900 mb-4">
{title}
</Dialog.Title>
)}
{children}
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
)
}
Design Token Patterns
Creating Consistent Design Tokens
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
900: '#1e3a8a',
},
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
},
},
spacing: {
'18': '4.5rem',
'88': '22rem',
},
borderRadius: {
'4xl': '2rem',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
},
},
}
Component Composition Best Practices
- Use the Compound Component Pattern for complex components
- Extract common patterns into reusable utilities
- Maintain consistent spacing using a spacing scale
- Use semantic color names instead of specific color values
- Create responsive defaults that work across devices
- Document component APIs with clear prop definitions
By following these patterns, you'll build a robust, maintainable component library that scales with your application while maintaining design consistency and developer productivity.