This library makes building design systems so much fun

This library makes building design systems so much fun

ยท

3 min read

Tailwind got me in the end

I'm officially a Tailwind CSS convert. I've been using it for two months at work, and I'm loving it. ๐Ÿ˜

I think it takes time to get around to it, but if your design system in Figma is using Tailwind, it'll make your life so much easier.

What's this post about?

I wanna dive into the library cva which I've enjoyed so much. It makes building design systems mega fun.

I wanna share how things would look like without it and how they do with it.

This will show you a glimpse of how much easier it is to build design systems with cva.

We're also gonna be using the cn utility function.

Let's build a button component without cva

We want to have a button component with different variants and sizes:

import React, { ComponentProps } from "react";
import { cn } from "@/lib/utils";

type ButtonProps = ComponentProps<"button"> & {
  variant?: "primary" | "secondary" | "danger";
  size?: "small" | "medium" | "large";
};

const Button = ({
  className,
  variant = "primary",
  size = "medium",
  ...props
}: ButtonProps) => {
  const buttonClasses = cn(
    "rounded font-semibold focus:outline-none",
    {
      "bg-blue-500 hover:bg-blue-600 text-white": variant === "primary",
      "bg-gray-200 hover:bg-gray-300 text-gray-800": variant === "secondary",
      "bg-red-500 hover:bg-red-600 text-white": variant === "danger",
      "px-4 py-2 text-sm": size === "small",
      "px-6 py-3 text-base": size === "medium",
      "px-8 py-4 text-lg": size === "large",
    },
    className
  );

  return <button className={buttonClasses} {...props} />;
};

export default Button;

I'm excluding forwardRef here. But if you need that, you'd have to add it. Although, it's gonna go away with React 19. ๐Ÿ˜„

The pain of building a component this way is all the conditional classes. The code becomes bloated and hard to read.

It would be nice to have a tiny thing responsible for abstracting this complexity.

cva to the rescue

import React from "react";
import { cva, VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const buttonVariants = cva("rounded font-semibold focus:outline-none", {
  variants: {
    variant: {
      primary: "bg-blue-500 hover:bg-blue-600 text-white",
      secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
      danger: "bg-red-500 hover:bg-red-600 text-white",
    },
    size: {
      small: "px-4 py-2 text-sm",
      medium: "px-6 py-3 text-base",
      large: "px-8 py-4 text-lg",
    },
  },
  defaultVariants: {
    variant: "primary",
    size: "medium",
  },
});

type ButtonProps = VariantProps<typeof buttonVariants> &
  React.ComponentProps<"button">;

const Button = ({
  className,
  variant,
  size,
  ...props
}: ButtonProps) => {
  return (
    <button
      className={cn(buttonVariants({ variant, size }), className)}
      {...props}
    />
  );
};

export default Button;

cva let's you define your variants and sizes in a single place. It abstracts the complexity of conditional classes.

The nice part here is that you don't need to worry about the typings. VariantProps<typeof buttonVariants> will take care of that for you. You can focus on the complexity of the designs in a different place. This place being the cva function.

It returns a function that you can call with the props you want to apply. It then returns the Tailwind classes you need.

Compound Variants

If you need styles to apply when multiple variants are met, you can do that with Compound Variants.

The confusion around variants

I'll be honest, variants is a bit confusing because you typically have a variant prop. But in the case of cva, variants are the props. Which would honestly be a better name for it.

ย