Step-by-Step Guide to Creating Scalable Tailwind Components with Tailwind Variants

Passionate Frontend Developer with 2.5 years of experience specializing in React.js and Next.js. Skilled in building responsive, user-friendly interfaces using Tailwind CSS. Enthusiastic about creating dynamic and engaging web applications.
📝 This is the second post in my Tailwind CSS blog series.
In the first post, we explored how to manage dynamic classes using Tailwind Safelist.
In this article, we’ll take it a step further by introducing tailwind-variants a powerful utility that simplifies class name management and helps you build scalable, variant-based component systems.
As a frontend developer working with Tailwind CSS, one of the most common challenges is managing conditional class names in components. When a UI component like a button has multiple states—say primary, secondary, or different sizes (sm, lg)—it can quickly lead to cluttered, hard-to-read class definitions directly in your JSX.
This is where tailwind-variants comes in—a utility that lets you create scalable and reusable component styles by abstracting Tailwind classes into a clean, declarative configuration.
In this blog, we will explore:
Why we need
tailwind-variantsInstallation and setup
Anatomy of a
tailwind-variantsDefinitionCreate a variant definition file
Use the Variant Function in Your Component
Dynamic Prop Passing and Component Usage
Compound Variants – Handling Complex Combinations
Summary – Why You Should Use
tailwind-variants
1. Why Do We Need tailwind-variants?
Let’s begin with a common use case: a button with multiple variants and sizes.
Before: Basic Tailwind with Conditional Logic:
// Example-1
<button
className={`
px-4 py-2 font-bold rounded
${variant === 'primary' ? 'bg-blue-500 text-white' : ''}
${variant === 'secondary' ? 'bg-gray-300 text-black' : ''}
${size === 'sm' ? 'text-sm' : ''}
${size === 'lg' ? 'text-lg' : ''}
`}
>
Click Me
</button>
// Example-2
<button className={'inline-flex items-center justify-center font-semibold rounded transition duration-200'}>
Click Me
</button>
These approaches work, but they break down when:
Component props increase
Logic clutters your JSX
Styles need to be reused across components
Testing and design consistency become important
What is tailwind-variants?
tailwind-variants is a utility that abstracts Tailwind class definitions into reusable, variant-based configurations.
Define your component styles once using the simple tv() API and reuse them consistently across your project.
After Using tailwind-variants:
// Example-1
<button className={button({ variant, size })}>
Click Me
</button>
// Example-2
<button className={button()}>
Click Me
</button>
2.📦 Installation
To begin using tailwind-variants, install the package:
npm install tailwind-variants
# or
yarn add tailwind-variants
No additional setup is required—it works seamlessly with Tailwind CSS.
3. Anatomy of a tailwind-variants Definition
Let's walk through how to define a component using the tv() function provided by the library.
Step 1: Create a variant definition file
Create a file button.ts:
// button.ts
import { tv } from 'tailwind-variants';
export const button = tv({
base: 'inline-flex items-center justify-center font-semibold rounded transition duration-200',
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-black hover:bg-gray-300',
outline: 'border border-blue-600 text-blue-600 hover:bg-blue-50',
},
size: {
sm: 'text-sm px-3 py-1.5',
md: 'text-base px-4 py-2',
lg: 'text-lg px-5 py-3',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
});
Property Breakdown:
base
The base property contains styles that are applied to all variants, no matter what the props are.
base: 'inline-flex items-center justify-center font-semibold rounded transition duration-200',
This ensures consistency in structure and layout — for example, padding, font, and layout utility classes that are shared.
variants
This is where you define all the different visual appearances your component can have. It accepts a nested object where each key (like variant, size, etc.) maps to options.
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-black hover:bg-gray-300',
outline: 'border border-blue-600 text-blue-600 hover:bg-blue-50',
},
size: {
sm: 'text-sm px-3 py-1.5',
md: 'text-base px-4 py-2',
lg: 'text-lg px-5 py-3',
},
}
Each variant name returns a set of Tailwind classes.
defaultVariants
This defines the default state for any variant you don’t explicitly pass.
defaultVariants: {
variant: 'primary',
size: 'md',
}
If you render the component without props, these defaults will apply automatically.
Step 2: Use the Variant Function in Your Component
Let’s now create a Button.tsx component that uses this variant configuration.
// Button.tsx
import React from 'react';
import { button } from './button';
type ButtonProps = {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
};
const Button = ({ variant, size, children }: ButtonProps) => {
return (
<button className={button({ variant, size })}>
{children}
</button>
);
};
export default Button;
Here:
The
button()function takes an object{ variant, size }and returns the appropriate class string.You pass the output to the
classNameof the<button>element.
Output Comparison
If you call:
<Button variant="secondary" size="lg">Submit</Button>
It will generate the following class string:
inline-flex items-center justify-center font-semibold rounded transition duration-200 bg-gray-200 text-black hover:bg-gray-300 text-lg px-5 py-3
If you call it without any props:
<Button>Click Me</Button>
It uses the defaults (variant: primary, size: md):
inline-flex items-center justify-center font-semibold rounded transition duration-200 bg-blue-600 text-white hover:bg-blue-700 text-base px-4 py-2
Step 3: Dynamic Prop Passing and Component Usage
You can now use this Button component throughout your application like so:
<Button variant="outline" size="sm">Outlined</Button>
<Button variant="primary">Default Size</Button>
<Button size="lg">Primary Large</Button>
<Button>Default Button</Button>
No matter how many times you reuse this button, the styling logic remains centralized and consistent.
4. Compound Variants – Handling Complex Combinations
While variants help define styles based on individual props like variant or size, there are situations where a combination of these props requires its own unique styling. This is where compound variants shine.
🔍 What Are Compound Variants?
Compound variants allow you to define class names that apply only when specific combinations of variant props are used. They help you:
Avoid nesting conditionals manually
Handle design exceptions cleanly
Improve readability without bloating your base styles
Use Case Example
Let’s say your design requires the outline variant with lg size to have some additional spacing or unique behavior — but only for this combination. You can express that using compoundVariants.
Update Your Variant Definition with Compound Variants
Let’s modify our original button.ts to include a compound variant:
// button.ts
import { tv } from 'tailwind-variants';
export const button = tv({
base: 'inline-flex items-center justify-center font-semibold rounded transition duration-200',
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-black hover:bg-gray-300',
outline: 'border border-blue-600 text-blue-600 hover:bg-blue-50',
},
size: {
sm: 'text-sm px-3 py-1.5',
md: 'text-base px-4 py-2',
lg: 'text-lg px-5 py-3',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
// 💡 Here's the compoundVariants section
compoundVariants: [
{
variant: 'outline',
size: 'lg',
class: 'shadow-md ring-2 ring-blue-200',
},
],
});
Output Example
If you now use:
<Button variant="outline" size="lg">Special Outline</Button>
It will generate the following Tailwind class string:
inline-flex items-center justify-center font-semibold rounded transition duration-200
border border-blue-600 text-blue-600 hover:bg-blue-50
text-lg px-5 py-3
shadow-md ring-2 ring-blue-200
Notice the
shadow-md ring-2 ring-blue-200is only added whenvariant="outline"andsize="lg"are both applied.
Why Compound Variants Matter
Using compoundVariants offers several benefits:
Avoids clutter in your JSX by removing logic like:
className={`${variant === 'outline' && size === 'lg' ? 'ring-2' : ''}`}Promotes DRY principles by keeping design logic in one place
Easier to scale when more combinations need special styling
Works seamlessly with
defaultVariantsand fallback values
Multiple Compound Variants
You can define multiple compound variants in the same array:
compoundVariants: [
{
variant: 'outline',
size: 'lg',
class: 'shadow-md ring-2 ring-blue-200',
},
{
variant: 'secondary',
size: 'sm',
class: 'opacity-80',
},
],
Each combination will be automatically applied when its conditions match.
With tailwind-variants, you're not just simplifying your className logic — you're building a scalable, consistent system for UI styling.
Adding compoundVariants takes it even further, giving you the flexibility to handle design nuances without sacrificing code readability or maintainability.
5. Summary – Why You Should Use tailwind-variants
Simplifies messy conditional class logic in JSX
Centralize your style definitions for easier reuse and consistency
Promotes reusability and consistent UI design
Supports advanced styling with compound variants
Makes it easier to build and manage a design system
💬 Join the Conversation
Let’s exchange ideas, lessons, and solutions—connect with me on LinkedIn or drop a comment below.


