If you’ve been using Tailwind CSS for a while, you’ve likely felt the pain of **long, repetitive class strings**. As your components grow, so does the mess:
<button className="bg-blue-500 text-white px-4 py-2 rounded-full font-medium hover:bg-blue-600">
Click me
</button>
Now imagine 10 buttons, each slightly different. That’s a **lot** of copy-pasting. More code. More bugs. Less clarity.
Let’s fix that.
Introducing tailwind-variants — a lightweight, powerful tool to help you write cleaner, more reusable Tailwind components using prop-based styling
🚀 Why Use `tailwind-variants`?
Tailwind is great for rapid styling, but it’s missing structured logic. `tailwind-variants` adds that missing layer:
- ✅ Centralized styling logic
- ✅ Prop-based customization
- ✅ Cleaner, readable, reusable classes
It brings the power of component libraries without losing the flexibility of Tailwind.
🧠 What is `tv()`?
At the heart of tailwind-variants is the tv() function:
import { tv } from "tailwind-variants";
const button = tv({
base: "font-medium rounded-full",
});
You define the **base styles** every version of the component will have.
🎛️ Adding Variants
const button = tv({
base: "font-medium rounded-full",
variants: {
size: {
sm: "px-2 py-1 text-sm",
lg: "px-4 py-2 text-lg",
},
color: {
primary: "bg-blue-500 text-white",
secondary: "bg-gray-200 text-black",
},
},
});
Now you can use props to style:
<button className={button({ size: "lg", color: "primary" }) }>
Click Me
</button>
Output:
<button class="font-medium rounded-full px-4 py-2 text-lg bg-blue-500 text-white">
Click Me
</button>
🧰 Default Variants
Set fallback values for props using `defaultVariants`:
defaultVariants: {
size: "sm",
color: "primary",
}
Now this works too:
<button className={button() }>
Click me
</button>
Result:
<button class="font-medium rounded-full px-2 py-1 text-sm bg-blue-500 text-white">
Click me
</button>
🔀 Compound Variants
Need to apply a style **only** when multiple props are combined? Use `compoundVariants`:
compoundVariants: [
{
size: "lg",
color: "primary",
class: "shadow-lg",
},
]
Now this will include the shadow:
<button className={button({ size: "lg", color: "primary" }) }>
Click Me
</button>
🧩 Styling Subcomponents with `slots`
Got multi-part components like cards, modals, or complex buttons? Use `slots` to define styles for each internal piece:
const card = tv({
slots: {
base: "bg-white p-4",
title: "text-lg font-bold",
content: "text-sm text-gray-700",
},
});
Usage:
const { base, title, content } = card();
return (
<div className={base() }>
<h2 className={title() }>Card Title</h2>
<p className={content() }>Card content goes here.</p>
</div>
);
🎯 Overriding Styles
You can override classes easily using the `class` prop:
button({
color: "secondary",
class: "bg-pink-500 hover:bg-pink-500",
});
Output:
<button class="font-semibold text-white px-3 py-1 rounded-full bg-pink-500 hover:bg-pink-500">
🛠 Creating a Real Button Component
const button = tv({
base: "inline-flex items-center gap-2 rounded-full font-medium",
variants: {
size: {
sm: "px-2 py-1 text-sm",
lg: "px-4 py-2 text-lg",
},
color: {
primary: "bg-blue-500 text-white",
secondary: "bg-gray-200 text-black",
},
},
defaultVariants: {
size: "sm",
color: "primary",
},
});
export function Button({ size, color, children }) {
return (
<button className={button({ size, color }) }>
{children}
</button>
);
}
📦 Final Thoughts
`tailwind-variants` adds superpowers to your Tailwind codebase:
- 🧠 Less duplication
- ✅ Better structure
- 🎯 Easier component scaling
It’s time to stop copying long strings and start building logic-driven design systems.