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={`rounded-lg border border-gray-200 bg-white shadow-sm ${className}`}
{...props}
>
{children}
</div>
);
}
function CardHeader({ children, className = "" }) {
return (
<div className={`border-b border-gray-200 px-6 py-4 ${className}`}>
{children}
</div>
);
}
function CardContent({ children, className = "" }) {
return <div className={`px-6 py-4 ${className}`}>{children}</div>;
}
function CardFooter({ children, className = "" }) {
return (
<div
className={`rounded-b-lg border-t border-gray-200 bg-gray-50 px-6 py-4 ${className}`}
>
{children}
</div>
);
}
// Usage
function ProductCard({ product }) {
return (
<Card className="transition-shadow hover:shadow-md">
<CardHeader>
<img
src={product.image}
alt={product.name}
className="h-48 w-full rounded-t-lg object-cover"
/>
</CardHeader>
<CardContent>
<h3 className="mb-2 text-lg font-semibold text-gray-900">
{product.name}
</h3>
<p className="mb-4 text-sm text-gray-600">{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="h-4 w-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="flex items-center gap-1 text-sm text-red-600">
<ExclamationCircleIcon className="h-4 w-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="mx-1 h-4 w-4 text-gray-400" />
)}
{item.href ? (
<Link
href={item.href}
className="text-sm font-medium text-gray-500 transition-colors hover:text-gray-700"
>
{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={`border-b-2 px-1 py-2 text-sm font-medium transition-colors ${
activeTab === item.id
? "border-blue-500 text-blue-600"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
}`}
>
{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 tracking-wider text-gray-500 uppercase"
>
{column.label}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
{data.map((row, index) => (
<tr key={index} className="hover:bg-gray-50">
{columns.map((column) => (
<td
key={column.key}
className="px-6 py-4 text-sm whitespace-nowrap 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="bg-opacity-25 fixed inset-0 bg-black" />
</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="mb-4 text-lg leading-6 font-medium text-gray-900">
{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.