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.