Components and variants
Components instead of @apply, variants, cn/tailwind-merge, dynamic classes.
Updated
The problem
With Tailwind, the same set of classes (a button, a card) shows up dozens of times. How do you reuse them without copying them everywhere? There are several options — only one is recommended.
| Option | Example | Verdict |
|---|---|---|
| copy-paste | the same 10 classes in 30 places | ✗ impossible to change |
@apply in CSS |
.btn { @apply px-4 py-2 } |
✗ you reinvent classic CSS, losing the benefits |
| a component | <Button variant="primary"> |
✓ recommended |
| loops / map | {items.map(i => <li className="...">)} |
✓ the classes appear only once in the code |
Reuse = components, not @apply
/* ✗ you reinvent classic CSS, losing the benefits */
.btn { @apply rounded-md px-4 py-2 bg-accent; }// ✓ the component is the unit of reuse
export function Button(props) {
return <button className="rounded-md px-4 py-2 bg-accent text-white" {...props} />
}@apply is only OK for styles you don't control through components (content from a CMS, external libraries).
Variants — the basic pattern
Exactly what shared/ui/button.tsx in this project does:
const variants = {
primary: 'bg-accent text-accent-fg hover:bg-accent-hover',
secondary: 'border border-line-strong bg-surface hover:bg-hover',
ghost: 'text-muted hover:bg-hover',
}
const sizes = { sm: 'h-8 px-3', md: 'h-9 px-4' }
<button className={cn(base, variants[variant], sizes[size], className)} />The variants are finite and named — you don't accept color="#f00" as a prop.
cn, clsx, tailwind-merge
clsx/ a simplecn— joins classes conditionally.tailwind-merge— resolves conflicts:twMerge('px-2', 'px-4')→'px-4'. Useful when you allow aclassNamefrom outside that overrides.cva(class-variance-authority) /tailwind-variants— a declarative API for variants, useful when you have many combinations.
Dynamic classes — the trap
Tailwind detects classes by reading the files' text. It can't guess classes that are built:
`bg-${color}-500` // ✗ doesn't exist in the final CSS
const colors = { red: 'bg-red-500', blue: 'bg-blue-500' }
colors[color] // ✓ complete classes, written literallyStyles from dynamic data
Values computed at runtime → style or a CSS variable:
<div className="w-(--progress) bg-success" style={{ '--progress': `${pct}%` }} />Summary
- The unit of reuse is the component, not
@apply. - Finite, named variants in an object;
cnfor combining,tailwind-mergefor conflicts. - Classes must be written in full in the code — Tailwind doesn't see classes built from pieces.