Skip to main content

Command Palette

Search for a command to run...

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

Published
7 min read
Step-by-Step Guide to Creating Scalable Tailwind Components with Tailwind Variants
K

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-variants

  • Installation and setup

  • Anatomy of a tailwind-variants Definition

    • Create 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 className of 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-200 is only added when variant="outline" and size="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 defaultVariants and 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.