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} />
}
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
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' },
},
},
},
},
}
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'>
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'>
// 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>
)
}
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>
)
}
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)