DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Tailwind CSS Patterns That Scale: CVA, Design Tokens, Dark Mode, and Component Architecture

Tailwind CSS gets criticized for making HTML verbose. That criticism misses the point. The real productivity gain is eliminating the cognitive overhead of naming things and switching files. These patterns keep your Tailwind components readable and maintainable.

Class Variance Authority (CVA)

CVA is the missing piece for component variants in Tailwind:

import { cva, type VariantProps } from 'class-variance-authority'

const button = cva(
  'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:opacity-50 disabled:pointer-events-none',
  {
    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 hover:bg-gray-100',
        ghost: 'hover:bg-gray-100 hover:text-gray-900',
      },
      size: {
        sm: 'h-8 px-3 text-sm',
        md: 'h-10 px-4',
        lg: 'h-12 px-6 text-lg',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'md',
    },
  }
)

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

export function Button({ variant, size, className, ...props }: ButtonProps) {
  return <button className={button({ variant, size, className })} {...props} />
}
Enter fullscreen mode Exit fullscreen mode

Usage: <Button variant='destructive' size='sm'>Delete</Button> — fully typed, no string manipulation.

The cn Utility

Merge Tailwind classes safely (handles conflicts):

import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

// Without cn: both p-4 and p-2 appear, browser applies p-4 (specificity lottery)
// With cn: twMerge resolves conflicts, last one wins
cn('p-4', 'p-2')  // => 'p-2'
cn('text-gray-500', isError && 'text-red-500')  // conditional classes
Enter fullscreen mode Exit fullscreen mode

Design Token System

Define your brand in tailwind.config.ts, not inline:

// tailwind.config.ts
export default {
  theme: {
    extend: {
      colors: {
        brand: {
          50: '#eff6ff',
          500: '#3b82f6',
          900: '#1e3a5f',
        },
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
        mono: ['JetBrains Mono', 'monospace'],
      },
      spacing: {
        '18': '4.5rem',
        '88': '22rem',
      },
      animation: {
        'fade-in': 'fadeIn 0.2s ease-in-out',
        'slide-up': 'slideUp 0.3s ease-out',
      },
      keyframes: {
        fadeIn: {
          '0%': { opacity: '0' },
          '100%': { opacity: '1' },
        },
        slideUp: {
          '0%': { transform: 'translateY(8px)', opacity: '0' },
          '100%': { transform: 'translateY(0)', opacity: '1' },
        },
      },
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

Responsive Design Patterns

Tailwind's mobile-first breakpoints:

// Grid that stacks on mobile, 3-col on desktop
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6'>

// Text that scales up on larger screens
<h1 className='text-2xl md:text-4xl lg:text-5xl font-bold'>

// Sidebar hidden on mobile, visible on desktop
<aside className='hidden lg:block w-64 shrink-0'>

// Padding that increases on larger screens
<section className='px-4 sm:px-6 lg:px-8 py-12 lg:py-24'>
Enter fullscreen mode Exit fullscreen mode

Dark Mode

// tailwind.config.ts
export default {
  darkMode: 'class', // or 'media' for system preference
}

// Components use dark: prefix
// <div className='bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100'>
Enter fullscreen mode Exit fullscreen mode
// Toggle dark mode
import { useTheme } from 'next-themes'

function ThemeToggle() {
  const { theme, setTheme } = useTheme()
  return (
    <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
      {theme === 'dark' ? 'Light' : 'Dark'}
    </button>
  )
}
Enter fullscreen mode Exit fullscreen mode

Avoiding Class Bloat

Extract repeated patterns into components, not CSS:

// Bad -- same classes repeated everywhere
<div className='rounded-xl border border-gray-200 bg-white p-6 shadow-sm'>
<div className='rounded-xl border border-gray-200 bg-white p-6 shadow-sm'>

// Good -- extract to a Card component
function Card({ children, className }: { children: React.ReactNode; className?: string }) {
  return (
    <div className={cn('rounded-xl border border-gray-200 bg-white p-6 shadow-sm', className)}>
      {children}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The AI SaaS Starter at whoffagents.com ships with Tailwind fully configured: CVA component variants, cn utility, design tokens, dark mode, and a complete shadcn/ui component set. $99 one-time.

Top comments (0)