Building Scalable Design Systems with Tailwind CSS and CVA

Building Scalable Design Systems with Tailwind CSS and CVA

The UI Consistency Challenge

As applications grow, keeping buttons, badges, and form elements styled consistently becomes difficult. Writing raw utility classes repeatedly leads to duplicate code and style fragmentation.


Solution: Class Variance Authority (CVA)

class-variance-authority (CVA) allows developers to define component variants declaratively with strict TypeScript types.


Building a Reusable Button Component

import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-xl font-medium transition-all focus:outline-none disabled:opacity-50',
  {
    variants: {
      variant: {
        primary: 'bg-blue-600 text-white hover:bg-blue-700 shadow-md',
        secondary: 'bg-neutral-200 text-neutral-900 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-white',
        outline: 'border-2 border-neutral-300 hover:bg-neutral-100 dark:border-neutral-700 dark:hover:bg-neutral-800',
      },
      size: {
        sm: 'h-8 px-3 text-xs',
        md: 'h-10 px-4 text-sm',
        lg: 'h-12 px-6 text-base',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {}

export function Button({ className, variant, size, ...props }: ButtonProps) {
  return (
    <button className={twMerge(clsx(buttonVariants({ variant, size }), className))} {...props} />
  );
}

Summary

Combining CVA with tailwind-merge provides type safety, eliminates class duplication, and establishes clean design tokens.