f in x
Tailwind CSS with React — Managing Dynamic ClassNames and Variants with CVA
> cd .. / HUB_EDITORIALE
Design, Web & Comunicazione

Tailwind CSS with React — Managing Dynamic ClassNames and Variants with CVA

[2026-06-30] Author: Ing. Calogero Bono
Zenithby Meteora Web The operating system for your business. Social, clients, bookings and invoices in one platform. Gyms, barbers, professionals. Discover Zenith Free demo · no card

You have a React component that needs to show a primary button, secondary, error, large, small, with an icon. You start concatenating className with ternary operators, conditional objects, template strings. After a few variants the code becomes unreadable, errors hide in details, and every change requires re-reading everything. Exactly: the problem isn't Tailwind, it's how you handle variants.

We, at Meteora Web, have been working with React and Tailwind for years. We've seen teams struggle with mile-long inline classes. Then we discovered CVA — Class Variance Authority — and managing variants became a whole new story. In this guide we'll see how to handle dynamic className in React with Tailwind, and why CVA is the right tool to keep your sanity.

Why Are Dynamic ClassNames in React with Tailwind a Problem?

Tailwind works with utility classes: bg-blue-500 text-white px-4 py-2 rounded. In React, when a component needs to change appearance based on props or state, you have to generate different class strings. The simplest way is to write directly in JSX:

function Button({ variant, size, children }) {
  let className = 'font-semibold rounded focus:outline-none';
  if (variant === 'primary') className += ' bg-blue-600 text-white hover:bg-blue-700';
  if (variant === 'secondary') className += ' bg-gray-200 text-gray-800 hover:bg-gray-300';
  if (size === 'sm') className += ' px-2 py-1 text-sm';
  if (size === 'lg') className += ' px-6 py-3 text-lg';
  return <button className={className}>{children}</button>;
}

The problem? This approach does not scale. Add a 'danger' variant, a disabled state, a loading spinner — code gets longer, conditions duplicate, and readability plummets. It's easy to miss a case or accidentally override a class. In real-world projects, we've seen components with over 50 lines of class logic. Not maintainable.

Sponsored Protocol

Common mistakes with dynamic className

  • Accidental override: string concatenation can lose base classes if an if is not executed.
  • Inconsistencies: changing a variant requires updating multiple points.
  • Reusability difficulty: extracting logic into a helper hook becomes messy quickly.

The solution is to centralize variants in one place, with a clear and predictable syntax. This is where CVA comes in.

What is CVA (Class Variance Authority) and How Does It Work?

CVA is a lightweight library (~1KB) that lets you define component variants declaratively. Instead of writing ifs, you use an object that maps each variant to a set of classes. The result is a function that, given the variant values, returns the correct className string. It works perfectly with Tailwind because classes are strings.

Sponsored Protocol

Simple installation:

npm install class-variance-authority

Or:

yarn add class-variance-authority

Now let's define the same button with CVA:

import { cva } from 'class-variance-authority';

const buttonVariants = cva(
  'font-semibold rounded focus:outline-none', // base classes
  {
    variants: {
      variant: {
        primary: 'bg-blue-600 text-white hover:bg-blue-700',
        secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
        danger: 'bg-red-600 text-white hover:bg-red-700',
      },
      size: {
        sm: 'px-2 py-1 text-sm',
        md: 'px-4 py-2 text-base',
        lg: 'px-6 py-3 text-lg',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
);

function Button({ variant, size, className, children }) {
  return (
    <button className={buttonVariants({ variant, size, className })}>
      {children}
    </button>
  );
}

That's it. Now if you want a primary large button, call <Button variant='primary' size='lg'>Click</Button>. CVA handles combining base classes with selected variant classes, and also manages extra classes passed via className. No ternary, no ifs, no concatenation errors.

How CVA handles extra classes

The last parameter of buttonVariants is className: CVA automatically merges it at the end, allowing the consumer to add overrides. Essential for flexibility.

Sponsored Protocol

How to Use CVA with Tailwind for Complex Components?

Now that we have the basics, let's extend to components with multiple variants and states. Take a badge with color and shape variants, plus a disabled state.

const badgeVariants = cva(
  'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
  {
    variants: {
      color: {
        gray: 'bg-gray-100 text-gray-800',
        red: 'bg-red-100 text-red-800',
        yellow: 'bg-yellow-100 text-yellow-800',
        green: 'bg-green-100 text-green-800',
        blue: 'bg-blue-100 text-blue-800',
      },
      shape: {
        pill: 'rounded-full',
        square: 'rounded-md',
      },
      disabled: {
        true: 'opacity-50 cursor-not-allowed',
      },
    },
    defaultVariants: {
      color: 'gray',
      shape: 'pill',
    },
  }
);

function Badge({ color, shape, disabled, className, children }) {
  return (
    <span className={badgeVariants({ color, shape, disabled, className })}>
      {children}
    </span>
  );
}

Now we have a badge that supports five colors, two shapes, and a disabled state, with just a few lines of definition. Logic stays centralized and clear.

Sponsored Protocol

Handling boolean variants and compound variants

CVA also supports compound variants (specific combinations). For example, if we want a button with variant='danger' and size='lg' to have an extra border:

const buttonVariants = cva(
  '...',
  {
    variants: { ... },
    compoundVariants: [
      {
        variant: 'danger',
        size: 'lg',
        className: 'border-2 border-red-800',
      },
    ],
    defaultVariants: { ... },
  }
);

Useful for specific cases without multiplying variants.

What Alternative to CVA? Comparison with clsx, tailwind-merge, and twMerge

Before CVA, many used clsx to merge conditional classes, or tailwind-merge to resolve utility conflicts. CVA doesn't replace these tools entirely, but integrates with them: often you use cva together with twMerge to ensure that classes passed by the consumer correctly override those defined. Here's a common pattern:

import { cva } from 'class-variance-authority';
import { twMerge } from 'tailwind-merge';

const buttonVariants = cva('...', { ... });

function Button({ variant, size, className, ...props }) {
  return (
    <button
      className={twMerge(buttonVariants({ variant, size }), className)}
      {...props}
    />
  );
}

With twMerge we resolve conflicts like bg-blue-600 overridden by bg-red-600 passed externally. CVA takes care of variant logic, twMerge ensures correct class ordering.

Sponsored Protocol

A real-world example from our work

In an e-commerce project for a Sicilian client, we had a product card with over 10 variants (layout, theme color, availability status, discount, promotion badge). Before CVA, the component had 80 lines of ternaries. After CVA, 20 lines of definition and 5 of rendering. Maintenance and testing became trivial.

What to do now

  1. Install CVA in your React project: npm install class-variance-authority.
  2. Rewrite a simple component (e.g., Button) with CVA following the pattern above. Use defaultVariants to avoid undefined values.
  3. Integrate twMerge if you want to allow external overrides without conflicts: npm install tailwind-merge.
  4. Explore compoundVariants to handle special combinations without repeating code.
  5. Check the official documentation: cva.style/docs for more details.

And if you're starting with Tailwind and React, take a look at our deep dive on Tailwind CSS for modern UI — the foundation for understanding why we use this stack in real projects.

Dynamic classNames don't have to be a nightmare. With CVA, every component becomes predictable, testable, and easy to extend. And as we always say: if the code isn't clear, the problem isn't the dev — it's the tool you're using.

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere informatico, fondatore di Meteora Web e Zenith OS. System administrator e progettista di piattaforme, app e CMS proprietari, con esperienza in sviluppo full-stack, marketing digitale ed ecosistema Google.
[ Read Full Dossier ]

> METEORA_WEB // DIGITAL AGENCY

We build the digital presence your business deserves.

Websites, social media, online advertising, e-commerce and high-performance hosting, engineered with method by computer engineers in Sciacca, for all of Italy.

> MW_JOURNAL

> READ_ALL()