Tailwind Component Extraction Strategies
When to keep utilities inline, when to extract a component, and how to use @apply, variants, and cva without painting yourself into a corner.
What you'll learn
- ✓When to extract
- ✓@apply tradeoffs
- ✓Variant libraries
- ✓Composing classes safely
- ✓Avoiding premature abstraction
Prerequisites
- •Comfortable with JS
What and Why
Tailwind’s superpower is utility classes inline with markup. The downside is repetition: a button appears in twenty places, each with the same dozen classes. The temptation is to extract a .btn class or a <Button> component immediately. Done wrong, you trade one duplication problem for an abstraction problem that is harder to refactor.
This article walks through when extraction pays off, what to extract into, and how to keep variants tidy as the design grows.
Mental Model
Three levels of reuse: copy utilities (cheap, flexible), extract a component (clean API, friction to change), and extract CSS via @apply (looks like old-school CSS, fights with Tailwind’s purge model). Choose the lightest tool that solves today’s pain.
inline utilities -- good for one-offs, prototypes
|
React/Astro component -- shared behavior + styles
|
cva / tailwind-variants -- many variants, one API
|
@apply in CSS -- legacy markup or non-component contexts Hands-on Example
Start inline. When the same set of classes appears three or more times and is conceptually one thing, extract a component:
type Props = { children: React.ReactNode; tone?: 'primary' | 'ghost' };
export function Button({ children, tone = 'primary' }: Props) {
const base = 'inline-flex items-center px-4 py-2 rounded-lg font-medium';
const tones = {
primary: 'bg-brand-500 text-white hover:bg-brand-600',
ghost: 'bg-transparent text-fg hover:bg-bg/50',
};
return <button className={`${base} ${tones[tone]}`}>{children}</button>;
}
When variants multiply (size, tone, disabled, loading), reach for a variant helper such as cva or tailwind-variants:
import { cva } from 'class-variance-authority';
export const button = cva(
'inline-flex items-center font-medium rounded-lg transition',
{
variants: {
tone: {
primary: 'bg-brand-500 text-white hover:bg-brand-600',
ghost: 'bg-transparent hover:bg-bg/50',
},
size: { sm: 'px-3 py-1 text-sm', md: 'px-4 py-2' },
},
defaultVariants: { tone: 'primary', size: 'md' },
}
);
Then className={button({ tone, size })} keeps markup readable.
Common Pitfalls
A few patterns that look clever but hurt later:
- Reaching for
@applyfirst. It rebuilds the old CSS coupling Tailwind freed you from. Reserve it for things likeproseoverrides or third-party markup you cannot touch. - Extracting too early. A
<Card>that wraps onedivwithp-4 roundedadds indirection without value. Wait for the second or third use. - Conditional class strings. Plain template literals invite duplicates and conflicts. Use
clsxor a variant library plustailwind-mergeto dedupe. - Variant explosion. If
cvaconfig grows past a screen, split the component or rethink the API. - Leaking utility props. Allowing
<Button className="...">lets callers override anything. Sometimes you want that; often you do not. Decide deliberately.
Best Practices
Inline until it hurts. The Tailwind core team has said this for years and it remains the best heuristic. Three is the magic number: when the same combo appears three times and reads as a concept, extract.
Prefer components to global CSS. A React or Astro component travels with its types, its variants, and its props. A .btn class travels with nothing and breaks silently when class names change.
Combine cva with tailwind-merge so overrides win predictably. Keep base styles small and put differences in variants; this makes it easy to add a new tone without touching the base.
Review components quarterly. As the design system matures, some early extractions become awkward. Inline them back and re-extract with the patterns you now understand.
Wrap-up
Extraction is a tool, not a virtue. Stay inline as long as you can, lift to a component when reuse and variants justify it, and reach for cva when variants get complex. The result is a small set of expressive components that compose cleanly with the rest of your utility-class codebase.
Related articles
- Tailwind Tailwind Design System Patterns That Scale
Build a design system on top of Tailwind that stays consistent as the app grows. Tokens, components, variants, and the cva pattern explained.
- Tailwind Tailwind with Headless UI Tutorial
Use Headless UI's accessible React components alongside Tailwind utilities to build menus, dialogs, comboboxes, and toggles that match your design system exactly.
- Tailwind Tailwind with shadcn/ui: An Overview
A practical overview of how Tailwind CSS and shadcn/ui work together to give you a component library you can actually own and customize.
- Tailwind Tailwind Component Patterns: Buttons, Cards, and Forms
Practical patterns for building reusable buttons, cards, and form controls in Tailwind — including variants, the @apply debate, and when to extract a component.