Tailwind Patterns Skill
Utility-first CSS patterns for modern web applications
PRINCIPLES
- •Utility-first: Compose styles from utility classes
- •Mobile-first: Design for mobile, enhance for desktop
- •Component extraction: Extract repeated patterns
- •Design tokens: Use consistent spacing, colors, typography
UTILITY COMPOSITION
Class Merging with cn()
typescript
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Usage - handles conflicts correctly
<button className={cn(
'px-4 py-2 rounded bg-blue-500',
isActive && 'bg-blue-700',
className // allows override
)} />
Conditional Classes
typescript
// ✅ Clean conditional styling
<div className={cn(
'p-4 rounded-lg border',
variant === 'primary' && 'bg-blue-500 text-white',
variant === 'secondary' && 'bg-gray-100 text-gray-900',
disabled && 'opacity-50 cursor-not-allowed'
)} />
// ✅ With variants map
const variants = {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
danger: 'bg-red-500 text-white hover:bg-red-600',
} as const;
<button className={cn('px-4 py-2 rounded', variants[variant])} />
RESPONSIVE DESIGN
Breakpoint System
typescript
// Mobile-first breakpoints (min-width)
// sm: 640px | md: 768px | lg: 1024px | xl: 1280px | 2xl: 1536px
<div className={cn(
'flex flex-col', // Mobile: stack vertically
'md:flex-row', // Tablet+: horizontal
'lg:gap-8', // Desktop+: larger gap
)}>
<aside className="w-full md:w-64 lg:w-80">Sidebar</aside>
<main className="flex-1">Content</main>
</div>
// Grid responsive
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{items.map(item => <Card key={item.id} />)}
</div>
Container Patterns
typescript
// Centered container with responsive padding
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<main className="max-w-4xl mx-auto">Content</main>
</div>
// Full-width section with constrained content
<section className="w-full bg-gray-100">
<div className="container mx-auto py-12 px-4">
Section content
</div>
</section>
DARK MODE
Class Strategy
typescript
// tailwind.config.ts
export default {
darkMode: 'class', // or 'media' for system preference
}
// Component with dark mode
<div className={cn(
'bg-white text-gray-900',
'dark:bg-gray-900 dark:text-white'
)}>
<p className="text-gray-600 dark:text-gray-400">
Subtle text
</p>
</div>
// Theme toggle
function ThemeToggle() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
document.documentElement.classList.toggle('dark', theme === 'dark');
}, [theme]);
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
}
COMPONENT PATTERNS
Button Component
typescript
import { cn } from '@/lib/utils';
import { Slot } from '@radix-ui/react-slot';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive';
size?: 'sm' | 'md' | 'lg';
asChild?: boolean;
}
const buttonVariants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-500',
outline: 'border border-gray-300 bg-transparent hover:bg-gray-50 focus:ring-gray-500',
ghost: 'bg-transparent hover:bg-gray-100 focus:ring-gray-500',
destructive: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
};
const buttonSizes = {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4',
lg: 'h-12 px-6 text-lg',
};
export function Button({
className,
variant = 'primary',
size = 'md',
asChild = false,
...props
}: ButtonProps) {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(
'inline-flex items-center justify-center rounded-md font-medium',
'transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2',
'disabled:opacity-50 disabled:pointer-events-none',
buttonVariants[variant],
buttonSizes[size],
className
)}
{...props}
/>
);
}
Card Component
typescript
export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
'rounded-lg border bg-white shadow-sm',
'dark:bg-gray-800 dark:border-gray-700',
className
)}
{...props}
/>
);
}
export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('p-6 pb-4', className)} {...props} />;
}
export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
return <h3 className={cn('text-lg font-semibold', className)} {...props} />;
}
export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('p-6 pt-0', className)} {...props} />;
}
// Usage
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
</CardHeader>
<CardContent>
<p>Card content here</p>
</CardContent>
</Card>
Input Component
typescript
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
error?: boolean;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, error, ...props }, ref) => {
return (
<input
ref={ref}
className={cn(
'flex h-10 w-full rounded-md border px-3 py-2',
'bg-white text-sm placeholder:text-gray-400',
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent',
'disabled:cursor-not-allowed disabled:opacity-50',
'dark:bg-gray-800 dark:border-gray-700',
error && 'border-red-500 focus:ring-red-500',
className
)}
{...props}
/>
);
}
);
LAYOUT PATTERNS
Flexbox Layouts
typescript
// Center everything
<div className="flex items-center justify-center min-h-screen">
<Content />
</div>
// Space between header items
<header className="flex items-center justify-between px-4 h-16">
<Logo />
<Nav />
<UserMenu />
</header>
// Sticky footer layout
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-1">{children}</main>
<Footer />
</div>
Grid Layouts
typescript
// Auto-fit responsive grid
<div className="grid grid-cols-[repeat(auto-fit,minmax(280px,1fr))] gap-6">
{items.map(item => <Card key={item.id} />)}
</div>
// Sidebar + content layout
<div className="grid grid-cols-1 lg:grid-cols-[240px_1fr] gap-6">
<aside>Sidebar</aside>
<main>Content</main>
</div>
// Dashboard grid
<div className="grid grid-cols-12 gap-4">
<div className="col-span-12 lg:col-span-8">Main content</div>
<div className="col-span-12 lg:col-span-4">Sidebar</div>
</div>
ANIMATION PATTERNS
Transitions
typescript
// Smooth hover effects
<button className={cn(
'transition-all duration-200 ease-in-out',
'hover:scale-105 hover:shadow-lg',
'active:scale-95'
)}>
Click me
</button>
// Fade in
<div className="animate-in fade-in duration-500">
Content
</div>
// Custom animation
// tailwind.config.ts
{
theme: {
extend: {
animation: {
'slide-up': 'slideUp 0.3s ease-out',
},
keyframes: {
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
},
},
},
}
ANTI-PATTERNS
❌ Avoid
typescript
// ❌ Inline styles with Tailwind
<div className="p-4" style={{ marginTop: '20px' }}>
// ❌ Too many arbitrary values
<div className="w-[347px] h-[89px] mt-[23px]">
// ❌ Not using design tokens
<div className="text-[#3b82f6]"> // Use text-blue-500
// ❌ Forgetting dark mode
<div className="bg-white text-black"> // No dark variant
✅ Prefer
typescript
// ✅ Consistent spacing scale
<div className="p-4 mt-5">
// ✅ Use design tokens
<div className="text-blue-500 dark:text-blue-400">
// ✅ Extend config for custom values
// tailwind.config.ts
{
theme: {
extend: {
spacing: { '18': '4.5rem' }
}
}
}
QUICK REFERENCE
| Utility | Class |
|---|---|
| Flexbox center | flex items-center justify-center |
| Absolute center | absolute inset-0 m-auto |
| Truncate text | truncate or line-clamp-2 |
| Screen reader only | sr-only |
| Focus ring | focus:ring-2 focus:ring-blue-500 |
| Smooth transition | transition-all duration-200 |