Class Variance Authority for React Components Variants

August 08, 2024

Introduction

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

Class Variance Authority for React Components Variants

In this post, we'll learn how to implement component variations using a very useful library for managing these variations—Class Variance Authority (CVA).

💡 Intended Audience

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.

button.tsx
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 and sizeStyles, 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 for sizeClass, 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.

button.tsx
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>
Class Variance Authority for React Components Variants

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.

button.tsx
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! 😊