Tailwind CSS Best Practices — Scalable Component Patterns
Advertisement
Introduction
Why This Matters
Tailwind is easy to start with but easy to misuse at scale. Long, duplicated class strings across dozens of components, inconsistent spacing, and dynamic class generation bugs are the most common failure modes. These patterns prevent all of them.
Component Extraction — The Primary Pattern
Never duplicate long class strings. Extract Tailwind classes into reusable React components:
// components/ui/button.tsx
import { cn } from '@/lib/utils'
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: 'primary' | 'secondary' | 'ghost' | 'danger'
size?: 'sm' | 'md' | 'lg'
loading?: boolean
}
const variants = {
primary: 'bg-blue-600 hover:bg-blue-700 text-white shadow-sm',
secondary: 'bg-white hover:bg-gray-50 text-gray-900 border border-gray-200',
ghost: 'hover:bg-gray-100 text-gray-700',
danger: 'bg-red-600 hover:bg-red-700 text-white shadow-sm'
}
const sizes = {
sm: 'h-8 px-3 text-sm rounded',
md: 'h-10 px-4 text-sm rounded-md',
lg: 'h-12 px-6 text-base rounded-lg'
}
export function Button({
variant = 'primary',
size = 'md',
loading = false,
disabled,
className,
children,
...props
}: ButtonProps) {
return (
<button
disabled={disabled || loading}
className={cn(
'inline-flex items-center justify-center gap-2 font-medium transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
'disabled:opacity-50 disabled:cursor-not-allowed',
variants[variant],
sizes[size],
className
)}
{...props}
>
{loading && <span className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />}
{children}
</button>
)
}The cn utility merges class names and resolves Tailwind conflicts:
// lib/utils.ts
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}CVA — Class Variance Authority
CVA provides a type-safe, declarative way to define component variants:
import { cva, type VariantProps } from 'class-variance-authority'
const badge = cva(
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors',
{
variants: {
variant: {
default: 'bg-gray-100 text-gray-900',
secondary: 'bg-gray-100 text-gray-700',
success: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
danger: 'bg-red-100 text-red-800',
outline: 'border border-current'
},
size: {
sm: 'text-xs px-2 py-0.5',
md: 'text-sm px-3 py-1'
}
},
defaultVariants: {
variant: 'default',
size: 'sm'
}
}
)
type BadgeProps = React.HTMLAttributes<HTMLSpanElement> & VariantProps<typeof badge>
export function Badge({ variant, size, className, ...props }: BadgeProps) {
return <span className={cn(badge({ variant, size }), className)} {...props} />
}
// Usage
<Badge variant="success">Active</Badge>
<Badge variant="danger" size="md">Error</Badge>Safe Dynamic Class Names
Tailwind scans your source files for class names at build time. Dynamic strings are not detected:
// WRONG — Tailwind will not include these classes in the build
const color = `bg-${status}-500` // 'bg-green-500' never in bundle
// CORRECT — use complete class names in the source
const statusColors = {
active: 'bg-green-500',
pending: 'bg-yellow-500',
error: 'bg-red-500',
inactive: 'bg-gray-400'
}
const colorClass = statusColors[status]
<div className={colorClass}>...</div>Responsive Design Patterns
// Mobile-first grid that adapts to screen size
function ProductGrid({ products }: { products: Product[] }) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 p-4">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
// Responsive typography
function HeroSection() {
return (
<section className="py-16 md:py-24 px-4">
<h1 className="text-3xl md:text-5xl lg:text-6xl font-bold leading-tight">
Build faster with Tailwind
</h1>
<p className="mt-4 text-lg md:text-xl text-gray-600 max-w-2xl">
Utility-first CSS that scales with your team.
</p>
</section>
)
}Dark Mode
// Wrap in a theme provider that adds 'dark' class to html element
function ThemeAwareCard({ title, description }: { title: string; description: string }) {
return (
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg p-6">
<h3 className="text-gray-900 dark:text-gray-100 font-semibold">{title}</h3>
<p className="text-gray-500 dark:text-gray-400 mt-2 text-sm">{description}</p>
</div>
)
}Configure dark mode in Tailwind v4:
@import "tailwindcss";
@variant dark (&:where(.dark, .dark *));Common Mistakes
- Building entire pages with one-off class strings instead of extracted components
- Using string interpolation for dynamic class names — Tailwind's JIT engine cannot detect them
- Not using
tailwind-merge— conflicting classes likepx-2 px-4keep both without it - Placing custom CSS in
@layer utilitieswhen it belongs in@layer componentsor a React component
Best Practices
- Install
tailwind-mergeandclsxfrom day one — they are essential for component-based Tailwind - Define a design token system in
@themefor colors, spacing, and typography before writing components - Use CVA for components with multiple variants — it eliminates conditional string concatenation
- Audit your bundle with Tailwind's built-in mode to see which utilities are generated
Key Takeaways
- Extract repeated class strings into React components immediately — never copy-paste Tailwind class strings
tailwind-mergeresolves conflicting Tailwind utilities (e.g.,p-2 p-4becomes justp-4)- Dynamic class names must use complete strings — never build class names with string interpolation
- CVA (Class Variance Authority) provides type-safe, declarative variant management for complex components
- Always write Tailwind mobile-first: base classes apply to mobile, then use
sm:,md:,lg:prefixes - Dark mode works via the
dark:variant combined with adarkclass on a parent element - The
cn()utility combiningclsxandtailwind-mergeis the de facto standard in the React/Tailwind ecosystem - Tailwind's
@layer componentsis for repeated patterns that cannot be componentized (e.g., prose styles)
Advertisement