Composition Patterns
Build reusable component systems with cva, cn(), and variant patterns
What you'll learn
Use @apply for shared CSS extraction (sparingly)
Build reusable component patterns with cn()
Create type-safe variant systems with cva()
Choose between inline utilities, @apply, and cva
Create reusable utility combinations with plugin-based custom class extensions
Override third-party component styles cleanly without !important or deep selectors
cn() for Safe Class Merging
cn() = clsx (conditionals) + tailwind-merge (conflict resolution — last wins).
import { cn } from "@/lib/utils"
function Badge({ variant = "default", className, children }) {
return (
<span
className={cn(
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
variant === "default" && "bg-gray-100 text-gray-700",
variant === "success" && "bg-green-100 text-green-700",
className // caller overrides — tailwind-merge handles px conflicts
)}
>
{children}
</span>
)
}
cva() — Class Variance Authority
Structured variant API with TypeScript autocomplete via VariantProps.
import { cva, type VariantProps } from "class-variance-authority"
const button = cva(
"inline-flex items-center justify-center rounded-lg font-medium transition-colors",
{
variants: {
variant: {
primary: "bg-blue-500 text-white hover:bg-blue-600",
secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
ghost: "bg-transparent hover:bg-gray-100",
},
size: {
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-base",
lg: "h-12 px-6 text-lg",
},
},
compoundVariants: [
{ variant: "primary", size: "lg", className: "shadow-md" },
],
defaultVariants: { variant: "primary", size: "md" },
}
)
function Button({ variant, size, className, children }: ButtonProps) {
return (
<button className={cn(button({ variant, size }), className)}>
{children}
</button>
)
}
@apply — Use Sparingly
/* CSS boundaries only — styling markdown prose */
.prose h2 {
@apply mt-8 mb-4 text-2xl font-bold tracking-tight;
}
.prose p {
@apply mb-4 leading-relaxed text-gray-600;
}
.prose code {
@apply rounded bg-gray-100 px-1.5 py-0.5 font-mono text-sm;
}
Avoid this anti-pattern
Using @apply for every component defeats utility composability. Inline + cn()/cva is better. Reserve @apply for CSS boundaries (prose, third-party widgets).
Pattern Comparison
One-off elements, prototyping. No abstraction overhead. Fully visible styles.
<button class="inline-flex items-center rounded-lg bg-blue-500 px-4 py-2 text-white">Click</button>
Button Component System with Variants
Code
tsx
1function Button({ variant = "primary", size = "md", className, children }) {2return <button className={...}>{children}</button>3}Key Takeaways
Use cn() (clsx + tailwind-merge) for safe, conflict-free class merging
cva() provides structured, type-safe variant APIs with autocomplete
Compound variants handle special-case combinations cleanly
Avoid @apply for components — reserve for CSS boundaries only
Always accept className prop on components and merge with cn()