After a few days with Tailwind CSS you will notice the same class strings repeating: every primary button, every card, every input carries an identical utility list. This lesson covers the three ways to eliminate that duplication — components, the @apply directive, and multi-cursor editing discipline — and explains which to reach for first.
<button class="rounded-lg bg-indigo-600 px-4 py-2 font-semibold text-white hover:bg-indigo-700">Save</button>
<button class="rounded-lg bg-indigo-600 px-4 py-2 font-semibold text-white hover:bg-indigo-700">Publish</button>
<button class="rounded-lg bg-indigo-600 px-4 py-2 font-semibold text-white hover:bg-indigo-700">Delete</button>Change the brand color and you are editing three places — or three hundred in a real app.
If you use React, Vue, Svelte, or any templating system, the answer is the same abstraction you already use for logic — a component:
function Button({ children }) {
return (
<button className="rounded-lg bg-indigo-600 px-4 py-2 font-semibold text-white hover:bg-indigo-700">
{children}
</button>
);
}The utility string now lives in exactly one file. This is the officially recommended approach because it also encapsulates structure and behavior, not just styles. Even in plain HTML projects, server-side includes or template partials achieve the same thing.
When you cannot use components — plain static HTML, CMS themes, Markdown renderers — @apply inlines utilities into a custom class in your CSS file:
@import "tailwindcss";
.btn-primary {
@apply rounded-lg bg-indigo-600 px-4 py-2 font-semibold text-white;
}
.btn-primary:hover {
@apply bg-indigo-700;
}
.card {
@apply rounded-xl border border-gray-200 bg-white p-6 shadow-sm;
}Now your markup is short again:
<button class="btn-primary">Save</button>
<div class="card">...</div>It is tempting to @apply everything and recreate semantic CSS — resist. If every element gets a custom class, you have re-imported all the problems Tailwind solved: naming fatigue, dead CSS, and styles far from markup. The Tailwind team advises using @apply sparingly, mainly for small, highly repeated primitives like buttons, badges, inputs, and cards. Layout should stay as utilities in the markup because it varies per page.
A reusable component usually needs to accept extra classes for context-specific tweaks:
function Button({ className = "", children }) {
return <button className={`btn-primary ${className}`}>{children}</button>;
}
<Button className="w-full mt-4">Checkout</Button>In larger React codebases, utilities like clsx and tailwind-merge handle conditional classes and resolve conflicts (for example when both px-4 and px-6 end up on one element, tailwind-merge keeps the last one).
Before abstracting, remember that repetition across two or three siblings is fine. Multi-cursor editing (select one class string, press your editor shortcut to select all occurrences) makes updating small repeated groups trivial. Abstract when a pattern crosses files, not the moment it appears twice.
@apply for that primitive only.@apply builds custom classes from utilities; reserve it for buttons, inputs, badges, and cards.@apply recreates the maintenance problems of traditional CSS.clsx and tailwind-merge keep dynamic class logic clean in React projects.Next up: Building a Complete Landing Page with Tailwind, where everything you have learned comes together.