Tame Long Class Lists the Right Way
Reusable Component Patterns
The cure for class soup is component extraction, not premature @apply. Build one source of truth for buttons and cards.
What you'll learn
- Extract repeated markup into a component or partial
- Keep one source of truth for a button or card
- Know the @apply versus component trade-off
When the same twelve-class button appears in forty files, the fix is not to hide the classes — it is to stop duplicating the markup. Component extraction beats premature @apply.
The Class Soup Problem
Copy-pasting a styled button means every visual tweak becomes a forty-file find-and-replace. Tailwind’s answer is the same as any DRY problem: extract a component.
function Button({ children }) {
return (
<button className="inline-flex items-center justify-center
rounded-md bg-blue-600 px-4 py-2 text-sm font-medium
text-white hover:bg-blue-700">
{children}
</button>
);
} Now the class list lives in exactly one place. Astro, Vue, and Svelte all support the same idea with their own component or partial syntax.
Variants via Props
Map a variant and size prop to full class strings. Use a lookup object so the strings stay statically analyzable:
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
ghost: 'bg-transparent text-blue-600 hover:bg-blue-50',
};
const sizes = { sm: 'px-3 py-1 text-sm', md: 'px-4 py-2 text-base' };
function Button({ variant = 'primary', size = 'md', ...props }) {
return (
<button
className={`rounded-md font-medium ${variants[variant]} ${sizes[size]}`}
{...props}
/>
);
} When @apply Is the Right Tool
@apply shines for markup you do not control — third-party HTML, Markdown output, or a CMS body. For your own components, prefer a real component so props, accessibility, and behavior travel with the styles.
/* You cannot add classes to rendered Markdown, so apply here */
.prose a {
@apply text-blue-600 underline underline-offset-2;
}