Component Patterns
Tailwind components pattern: when to extract, @apply trade-offs, cn helper, and the discipline that keeps utility-first sustainable.
Tailwind — components pattern
EXAMPLE
// ===== When utility-first goes wrong =====
// Markup like this every screen:
<button class="px-4 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 focus:ring-2 focus:ring-blue-400">
// Repeated 50 times. Copy-paste rot. Inconsistent variations.
// ===== Solution 1: component =====
// Extract to a React/Vue/Svelte component:
function PrimaryButton({ className, ...props }) {
return (
<button
className={cn('px-4 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 focus:ring-2 focus:ring-blue-400', className)}
{...props}
/>
);
}
// Usage:
<PrimaryButton onClick={save}>Save</PrimaryButton>
<PrimaryButton className="w-full" disabled={loading}>Submit</PrimaryButton>
// ===== Solution 2: cva (class-variance-authority) =====
import { cva } from 'class-variance-authority';
const button = cva('rounded-md font-semibold focus:outline-none focus:ring-2', {
variants: {
intent: {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-400',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-400',
ghost: 'bg-transparent text-slate-900 hover:bg-slate-100',
},
size: {
sm: 'text-sm py-1 px-3',
md: 'text-base py-2 px-4',
lg: 'text-lg py-3 px-6',
},
},
defaultVariants: { intent: 'primary', size: 'md' },
});
function Button({ intent, size, className, ...props }) {
return <button className={cn(button({ intent, size }), className)} {...props} />;
}
// ===== Solution 3: @apply (use sparingly) =====
@layer components {
.btn {
@apply px-4 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-700;
}
.btn-danger { @apply bg-red-600 hover:bg-red-700; }
}
<button class="btn">Save</button>
// Trade-off: leaves the utility-first mindset; defeats some build optimisations.
// Useful for design tokens that appear EVERYWHERE without variants.
// ===== shadcn/ui pattern (recommended) =====
// - Copy components into YOUR codebase (not a dependency)
// - Use cva for variants
// - className override always supported
// - Use Radix UI for accessible primitives + Tailwind for styling
// ===== When to extract =====
// Rule of three: extract a component when the pattern appears for the THIRD time.
// Before that, copy-paste is fine.
// ===== What to leave inline =====
// One-off layouts:
<div className="flex items-center gap-2 p-4">
// These are clearer inline than as 'CardHeader' helpers.
// ===== Patterns to internalise =====
// - Component for repeated UI (button, input, card)
// - cva for variants (intent / size / state)
// - className prop on every component for parent overrides
// - @apply only for truly universal design tokens
// - shadcn/ui style: copy + own the components
// ===== Pitfalls =====
// - Premature extraction -> wrong abstraction
// - @apply everywhere -> reinvents CSS
// - Component without className override -> consumers fight the styles
// - Long class strings without cn -> conflicts (use twMerge)
Why it matters
Utility-first stays sustainable when you extract repeated patterns into components, parameterise with cva variants, allow className overrides, and reach for @apply only sparingly. The shadcn/ui pattern (copy + own + cn helper) became the default for a reason.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// React: extract a reusable Button component, not 50 classes per call site.
const Button = ({ children }) => <button className="px-4 py-2 bg-emerald-600 text-white rounded">{children}</button>;
Try it Yourself »
Discussion
Loading…