Building Components
Long class lists repeat. Tailwind gives you two ways to reuse: extract the component, or extract a class with @apply.
Prefer Components
In React, Vue, Svelte, or Astro, wrap the markup in a component:
export function Button({ children, ...props }) {
return (
<button
className="rounded-lg bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700"
{...props}
>
{children}
</button>
);
}
Use it everywhere:
<Button>Save</Button>
<Button type="button">Cancel</Button>
The classes live in one place. This is the idiomatic Tailwind answer.
Variants with cva
For components with multiple looks, class-variance-authority keeps it tidy:
import { cva } from "class-variance-authority";
const button = cva("rounded-lg px-4 py-2 font-medium", {
variants: {
intent: {
primary: "bg-blue-600 text-white hover:bg-blue-700",
ghost: "text-blue-600 hover:bg-blue-50",
},
size: {
sm: "text-sm",
lg: "text-lg",
},
},
defaultVariants: { intent: "primary", size: "sm" },
});
button({ intent: "ghost", size: "lg" });
Conditional Classes with clsx
Merge classes based on state:
import clsx from "clsx";
<div className={clsx("p-4 rounded", isActive && "bg-blue-100", disabled && "opacity-50")}>
...
</div>
tailwind-merge resolves conflicts (p-2 vs p-4) when combining.
@apply for Shared Styles
When you genuinely need a named class — for prose, third-party markup, or a design token — use @apply:
@import "tailwindcss";
.btn {
@apply rounded-lg bg-blue-600 px-4 py-2 font-medium text-white;
}
.btn:hover {
@apply bg-blue-700;
}
Now class="btn" works in plain HTML.
When to Use Which
- Component — Most cases. Reuse structure and behavior.
@apply— Markdown content, generated HTML, or a stable public class.- cva/clsx — Components with variants or conditional styling.
Best Practices
- Components first —
@applyis a tool, not the default. - Keep variants declarative — cva over nested ternaries.
- Use
tailwind-merge— When composing classes from props. - Name by intent —
Button intent="danger", notButton red.
Common Mistakes
@applyeverywhere — You rebuild a CSS framework you left.- Concatenating classes — The scanner misses dynamic strings.
- Conflicting classes —
p-2 p-4; usetailwind-merge. - One giant component — Split variants instead of many booleans.