As React applications grow, maintaining consistent styling and component behavior becomes challenging. Today, I'll show you how to build truly reusable components using two powerful tools: the cn() utility function and Class Variance Authority (CVA).
Why This Approach Matters
Traditional component styling often leads to:
- Inconsistent design patterns across components
- Repetitive CSS class combinations
- Difficult prop-based styling management
- Poor TypeScript support for variant props
The cn() + CVA combo solves these issues elegantly.
Setting Up the Foundation
First, let's create our cn() utility function. This is a simple wrapper around clsx and tailwind-merge:
// lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
This function does two things:
- clsx - Conditionally joins class names together
- twMerge - Intelligently merges Tailwind classes, avoiding conflicts
Creating Your First CVA Component
Now let's build a Button component using Class Variance Authority:
// components/ui/button.tsx
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { ButtonHTMLAttributes, forwardRef } from "react";
// Define variants with CVA
const buttonVariants = cva(
// Base classes - applied to all buttons
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-blue-600 text-white hover:bg-blue-700",
destructive: "bg-red-600 text-white hover:bg-red-700",
outline: "border border-gray-300 bg-transparent hover:bg-gray-100",
ghost: "hover:bg-gray-100",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
// Create the component interface
interface ButtonProps
extends ButtonHTMLAttributes,
VariantProps {
asChild?: boolean;
}
const Button = forwardRef(
({ className, variant, size, ...props }, ref) => {
return (
The Magic Behind This Approach
Here's what makes this pattern so powerful:
1. Type Safety
CVA automatically generates TypeScript types for your variants. Your IDE will autocomplete variant options and catch invalid combinations.
2. Class Conflict Resolution
The cn() function intelligently merges classes. If you pass conflicting Tailwind classes, it keeps the last one:
// Without cn(): "bg-red-500 bg-blue-500" (both applied)
// With cn(): "bg-blue-500" (conflict resolved)
3. Flexible Composition
You can easily override or extend styles:
// Override padding while keeping other styles
// Add additional classes
Building More Complex Components
Let's create a Card component with multiple parts:
// components/ui/card.tsx import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; import { HTMLAttributes } from "react"; const cardVariants = cva( "rounded-lg border bg-white text-gray-950 shadow-sm", { variants: { variant: { default: "border-gray-200", destructive: "border-red-200 bg-red-50", success: "border-green-200 bg-green-50", }, size: { default: "p-6", sm: "p-4", lg: "p-8", }, }, defaultVariants: { variant: "default", size: "default", }, } ); interface CardProps extends HTMLAttributes, VariantProps {} function Card({ className, variant, size, ...props }: CardProps) { return (); } function CardHeader({ className, ...props }: HTMLAttributes) { return (); } function CardTitle({ className, ...props }: HTMLAttributes) { return (
); } function CardContent({ className, ...props }: HTMLAttributes) { return; } export { Card, CardHeader, CardTitle, CardContent };
Usage Examples
Now you can use these components with confidence:
function MyComponent() {
return (
{/* Basic usage */}
{/* Different variants */}
{/* Custom styling */}
{/* Card component */}
Success!
Your action completed successfully.
);
}
Pro Tips for Success
1. Keep Variants Focused
Don't create too many variants. Stick to the core use cases and let consumers handle edge cases with the className prop.
2. Use Compound Variants
CVA supports compound variants for complex combinations:
const buttonVariants = cva("base-classes", {
variants: {
variant: { default: "...", destructive: "..." },
size: { sm: "...", lg: "..." },
},
compoundVariants: [
{
variant: "destructive",
size: "lg",
class: "text-xl font-bold", // Special styling for large destructive buttons
},
],
});
3. Export Variants for Reuse
Export your variant functions so other components can reuse the same styling logic.
Wrapping Up
The combination of cn() and CVA gives you:
- ✅ Type-safe component APIs
- ✅ Consistent design system
- ✅ Flexible styling options
- ✅ Better developer experience
- ✅ Easier maintenance and scaling
Start with simple components like buttons and inputs, then gradually build your component library. Your future self (and your team) will thank you for the consistency and maintainability this approach provides.
The best part? This pattern scales beautifully from small projects to enterprise applications. Once you experience the developer experience improvement, you'll never want to go back to traditional component styling approaches.
