@apply Is Not How You Share Styles
NEVER: Collapse repeated markup into a `.btn { @apply … }` class to make it look "cleaner" — it reintroduces exactly what utilities removed: you invent a name, you jump between files to read the button's appearance, and one edit silently repaints every button. It also collapses at the first variant (`.btn-primary`, `.btn-sm`, disabled), where you start hand-rolling the variant system you already have. Extract a COMPONENT instead — a React component with CVA — which encapsulates behaviour too, not just the class string. `@apply` is legitimate only for markup you do not control (a `.prose` subtree, a third-party widget).
Extract a component when markup repeats, instead of collapsing utilities into a .btn class with @apply
/* no */ .btn { @apply inline-flex rounded-md px-4 py-2 …; }
// yes: const button = cva("inline-flex rounded-md", { variants: { size: { sm: "px-2 py-1", md: "px-4 py-2" } } });Bad
Good
Why it matters
The instinct is understandable: a long `class="inline-flex items-center rounded-md px-4 py-2 …"` looks like duplication, and `.btn { @apply … }` makes it go away. But the duplication was never the problem utility classes had — it was the naming, the indirection, and the coupling. `.btn` brings all three back. You now maintain a name that means nothing to the browser, you cannot read the button's appearance from the button's markup, and any change to `.btn` silently repaints every button in the product, which is exactly the cascade problem Tailwind exists to remove.
It also collapses the moment you need a variant: a `.btn-primary`, a `.btn-sm`, a disabled state, and you are hand-rolling the variant system Tailwind already ships. Tailwind's own guidance is a ladder, and `@apply` is not on it: if the repetition is in a list, use a loop; if it is a real UI element, extract a *component* — a React component, with CVA for the variants — because a component encapsulates the markup and the behaviour too, not just the class string.
`@apply` earns its place in one narrow case: styling markup you do not control (a `.prose` subtree, a third-party widget) or an unavoidable single element, where a component is not an option. Reach for it there, not to tidy up.