Introduction
When creating a React component, we often need to handle various component variations, such as a button with different colors and shapes.

In this post, we'll learn how to implement component variations using a very useful library for managing these variations—Class Variance Authority (CVA).
This blog post is intended for beginner-to-intermediate React and Typescript developers. Some knowledge of basic React and Typescript is assumed.
Components Variations without CVA
Let's start by trying to implement component variations without CVA. We can achieve this by using conditional class names within the component.
import * as React from 'react';
// get the type of the props
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
intent?: 'primary' | 'secondary' | 'tertiary';
size?: 'sm' | 'md' | 'lg';
}
export function Button({ intent, size, ...props }: ButtonProps) {
// define base styles
const baseStyles = 'rounded-xl px-4 py-2 font-medium transition-all border border-b-4 bg-gradient-to-b shadow-[inset_0px_1px_0px_1px] shadow-white/40 active:border-b active:shadow-black/40 disabled:pointer-events-none disabled:opacity-50';
// define the styles for each intent
const intentStyles = {
primary: 'text-white border-sky-900 from-sky-900 to-sky-700 hover:from-sky-950 hover:to-sky-800',
secondary: 'text-white border-rose-900 from-rose-900 to-rose-700 hover:from-rose-950 hover:to-rose-800',
tertiary: 'text-black border-slate-300 from-slate-200 to-slate-100 hover:from-slate-300 hover:to-slate-200',
};
// define the styles for each size
const sizeStyles = {
md: 'h-10 px-4 text-sm',
sm: 'h-8 px-4 text-xs',
lg: 'h-14 px-6 text-base',
};
// default values if intent or size are not passed in
intent = intent ?? 'primary';
size = size ?? 'md';
let compoundClass = '';
if (intent === 'secondary' && size === 'md') {
compoundClass = 'from-rose-700 to-rose-500';
}
if (intent === 'tertiary' && size === 'sm') {
compoundClass = 'hover:underline';
}
// get the styles for the intent and size
const intentClass = intentStyles[intent];
const sizeClass = sizeStyles[size];
// concatenate the styles together
const className = `${baseStyles} ${intentClass} ${sizeClass} ${compoundClass}`;
return <button {...props} className={className} />;
}
Now, let's break down the code snippet above:
- Here, we define
baseStyles
to serve as the foundational styling for our component. - Next, we define
intentStyles
andsizeStyles
, which will apply styling to the component based on the specified variations. - We also set default variations for when no specific variations are provided. For
intentClass
, we choose primary, and forsizeClass
, we select md as our default variations. - Additionally, we manage special cases for specific variations. For example, we might want the button to have an underline when hovered over in the 'tertiary' intent and 'sm' size, or make the button color slightly lighter when using the 'secondary' intent and 'md' size.
Let's try using our component and see how it looks like.
import { Button } from '@/components/button';
<Button>Button</Button>
<Button size='sm'>Button</Button>
<Button size='lg'>Button</Button>
<Button intent='secondary'>Button</Button>
<Button intent='secondary' size='sm'>Button</Button>
<Button intent='secondary' size='lg'>Button</Button>
<Button intent='tertiary'>Button</Button>
<Button intent='tertiary' size='sm'>Button</Button>
<Button intent='tertiary' size='lg'>Button</Button>

The main issue with handling component variations without CVA is the impact on readability and maintainability. As the codebase grows and we require more diverse variations, it becomes increasingly difficult to manage and understand the variations within our components. Wouldn’t it be nice if we had a function that could return the appropriate class names based on the variations we provide? CVA is the answer.
Components Variations with CVA
Class Variance Authority (CVA) simplifies defining component variants in a cleaner way. By centralizing the definition of variants, CVA enhances both the readability and maintainability of our codebase.
But first, we need to install CVA in our project.
npm install class-variance-authority
Now, let's start converting our component button variants using CVA.
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
[
// define base styles
'rounded-xl px-4 py-2 font-medium transition-all',
'border border-b-4',
'bg-gradient-to-b',
'shadow-[inset_0px_1px_0px_1px] shadow-white/40',
'active:border-b active:shadow-black/40',
'disabled:pointer-events-none disabled:opacity-50',
],
{
variants: {
// define the styles for each intent
intent: {
primary: [
'text-white',
'border-sky-900',
'from-sky-900 to-sky-700',
'hover:from-sky-950 hover:to-sky-800',
],
secondary: [
'text-white',
'border-rose-900',
'from-rose-900 to-rose-700',
'hover:from-rose-950 hover:to-rose-800',
],
tertiary: [
'text-black',
'border-slate-300',
'from-slate-200 to-slate-100',
'hover:from-slate-300 hover:to-slate-200',
],
},
// define the styles for each size
size: {
sm: 'h-8 px-4 text-xs',
md: 'h-10 px-4 text-sm',
lg: 'h-14 px-6 text-base',
},
},
defaultVariants: {
intent: 'primary',
size: 'md',
},
compoundVariants: [
{
intent: 'secondary',
size: 'md',
className: 'from-rose-700 to-rose-500',
},
{
intent: 'tertiary',
size: 'sm',
className: 'hover:underline',
},
],
}
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export function Button({ intent, size, ...props }: ButtonProps) {
return <button {...props} className={buttonVariants({ intent, size })} />;
}
Let's break down on CVA structure function.
- The
cva(…)
function accepts two parameters: the base style of our component and a configuration object where we define variants, default variants, and conditional variants. - The
variants
key is where we define our variants, such as intent and size variations for our component. - The
defaultVariants
key is where we define our default variants. - The
compoundVariants
key is used to manage conditional variants for our component.
By organizing our component variations in this way, CVA helps keep our code cleaner and more manageable, especially as the number of variations increases.
Conclusion
Although the manual approach can achieve the same results, using CVA helps us structure and organize our component design system more effectively. Implementing CVA will significantly enhance the readability and maintainability of our codebase. In real-world scenarios, CVA proves invaluable for maintaining design consistency across the application you're building. Happy Coding! 😊