Tailwind is great for small projects, but at scale you need patterns. Here's how we structure Tailwind in large codebases.
The Component Abstraction Pattern
// Don't repeat utility classes everywhere
// Bad
<button className="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg">Save</button>
<button className="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg">Submit</button>
// Good: extract to component
function Button({ children, variant = "primary", ...props }) {
const variants = {
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
};
return (
<button className={cn("font-medium py-2 px-4 rounded-lg", variants[variant])} {...props}>
{children}
</button>
);
}Design Tokens with CSS Variables
/* index.css */
:root {
--primary: 222 47% 31%;
--primary-foreground: 210 40% 98%;
--radius: 0.5rem;
--shadow-sm: 0 1px 2px hsl(0 0% 0% / 0.05);
}
.dark {
--primary: 217 91% 60%;
--primary-foreground: 222 47% 11%;
}The cn() Utility
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
function cn(...inputs) {
return twMerge(clsx(inputs));
}
// Handles conflicts intelligently
cn("px-4 py-2", "px-6") // → "py-2 px-6" (px-6 wins)CVA for Variant Management
import { cva } from "class-variance-authority";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md font-medium transition-colors",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
outline: "border border-input bg-background hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground",
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-10 px-4 text-sm",
lg: "h-12 px-8 text-base",
},
},
defaultVariants: { variant: "default", size: "md" },
}
);Anti-Patterns
- Don't use
@applyextensively — it defeats Tailwind's purpose - Don't hardcode colors — use semantic tokens
- Don't create utility classes with arbitrary values everywhere
- Don't ignore dark mode — build it in from the start