Reusability Without Class Bloat
Tame long class lists with the right boundary — usually a component, not a wrapper class.
Every Tailwind project eventually meets the same anxiety: the class lists are long. That is not a bug — it is the styling made visible. But there are honest ways to keep it manageable.
The strategies, best to worst for reuse
- Component / partial. Write the button once as a component; every use is
<Button>. The framework owns reuse. This is the answer 90% of the time. - Loop the markup. A nav is one link template mapped over data — the classes exist once in source.
- A variant helper (clsx / cva) to organize conditional classes into readable groups instead of a ternary soup.
- @utility or @apply for a genuinely global primitive, used sparingly.
Organize, do not just shorten
Long is fine if it is ordered. Group classes by concern — layout, then spacing, then color, then state — and a 15-class element becomes scannable. The Prettier Tailwind plugin does this sorting automatically.
The cure for class soup is a component boundary, not a new CSS class. If you are inventing .card-inner-wrapper to hide utilities, you have walked back into the problem Tailwind solved.Example
When to use it
- A React component library wraps every button in a <Button> component that hard-codes the Tailwind class list, so designers change one file to restyle every button site-wide.
- A Blade template partial in a Laravel app centralises card markup with its Tailwind classes, so the 50 places that render cards all stay in sync when the design changes.
- A Vue project avoids @apply for most things but uses it for a .btn-primary rule in one CSS file to give non-component contexts (e.g., Markdown-rendered HTML) a consistent button style.
More examples
Component boundary in React
Collapses the long class list into a single component; callers pass a variant prop, making design changes a one-file edit rather than a global search-and-replace.
const variants = {
primary: "bg-cyan-600 text-white hover:bg-cyan-700",
secondary: "border border-gray-300 text-gray-700 hover:bg-gray-50",
danger: "bg-red-600 text-white hover:bg-red-700",
};
export function Button({ variant = "primary", children, ...props }) {
return (
<button
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm
font-semibold transition-colors focus:outline-none
focus:ring-2 focus:ring-offset-2 ${variants[variant]}`}
{...props}
>
{children}
</button>
);
}Blade partial in Laravel
A Blade component keeps all card Tailwind classes in one partial; every call site is clean HTML-like syntax, and updating the card style touches only the one file.
{{-- resources/views/components/card.blade.php --}}
<div class="rounded-xl overflow-hidden shadow-md bg-white flex flex-col {{ $class ?? '' }}">
@if($image ?? false)
<div class="aspect-video">
<img src="{{ $image }}" alt="{{ $title }}" class="w-full h-full object-cover" />
</div>
@endif
<div class="p-4 flex flex-col flex-1">
<h3 class="text-base font-bold text-gray-900">{{ $title }}</h3>
<p class="mt-1 text-sm text-gray-500 flex-1">{{ $slot }}</p>
</div>
</div>
{{-- Usage --}}
<x-card title="Wireless Headphones" image="/img/headphones.jpg">
Crystal-clear audio, 30 h battery.
</x-card>Targeted @apply for non-component HTML
@apply is reserved for output HTML you cannot annotate directly (CMS/Markdown), while all interactive components keep their classes in the template for Tailwind's scanner to find.
/* styles/prose.css — applied to Markdown-rendered content only */
.prose-content a {
@apply text-cyan-600 underline hover:text-cyan-800 transition-colors;
}
.prose-content blockquote {
@apply border-l-4 border-cyan-300 pl-4 italic text-gray-600;
}
.prose-content code {
@apply font-mono text-sm bg-gray-100 rounded px-1 py-0.5 text-rose-600;
}
Discussion