Tailwind CSS
Utility-first CSS for styling React components.
Class Order Convention
Follow this order for consistency:
Layout → Sizing → Spacing → Typography → Colors → Effects → States
<div className="flex items-center justify-between w-full max-w-md p-4 gap-2 text-sm font-medium text-gray-900 bg-white rounded-lg shadow-sm hover:bg-gray-50 transition-colors">
Content
</div>The cn() Utility
Combine classes conditionally using clsx and tailwind-merge:
// utils/cn.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}Conditional Classes
import { cn } from '@/utils/cn';
interface ButtonProps {
variant: 'primary' | 'secondary' | 'ghost';
size: 'sm' | 'md' | 'lg';
isFullWidth?: boolean;
className?: string;
}
const Button = ({ variant, size, isFullWidth, className }: ButtonProps) => (
<button
className={cn(
// Base styles
'inline-flex items-center justify-center font-medium rounded-lg transition-colors',
// Focus styles
'focus:outline-none focus:ring-2 focus:ring-offset-2',
// Variants
{
'bg-orange-500 text-white hover:bg-orange-600 focus:ring-orange-500':
variant === 'primary',
'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-500':
variant === 'secondary',
'bg-transparent text-gray-600 hover:bg-gray-100 focus:ring-gray-500':
variant === 'ghost',
},
// Sizes
{
'px-3 py-1.5 text-sm': size === 'sm',
'px-4 py-2 text-base': size === 'md',
'px-6 py-3 text-lg': size === 'lg',
},
// Conditional
isFullWidth && 'w-full',
// External className (always last)
className
)}
/>
);Component Variants Pattern
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
// Base styles
'inline-flex items-center justify-center rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none',
{
variants: {
variant: {
primary: 'bg-orange-500 text-white hover:bg-orange-600',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
outline: 'border border-gray-300 bg-transparent hover:bg-gray-50',
ghost: 'bg-transparent hover:bg-gray-100',
destructive: 'bg-red-500 text-white hover:bg-red-600',
},
size: {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4 text-base',
lg: 'h-12 px-6 text-lg',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = ({ variant, size, className, ...props }: ButtonProps) => (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);Responsive Design
Mobile-first approach with breakpoint prefixes:
<div className="
grid
grid-cols-1 {/* Mobile: 1 column */}
sm:grid-cols-2 {/* Small: 2 columns */}
md:grid-cols-3 {/* Medium: 3 columns */}
lg:grid-cols-4 {/* Large: 4 columns */}
gap-4
">
{items.map((item) => (
<Card key={item.id} />
))}
</div>Dark Mode
<div className="bg-white dark:bg-gray-900">
<h1 className="text-gray-900 dark:text-white">
Title
</h1>
<p className="text-gray-600 dark:text-gray-400">
Description
</p>
</div>Common Patterns
Card
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-gray-900">
{/* Content */}
</div>Input
<input
className="w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 placeholder-gray-500 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
placeholder="Enter text..."
/>Badge
<span className="inline-flex items-center rounded-full bg-orange-100 px-2.5 py-0.5 text-xs font-medium text-orange-800 dark:bg-orange-900 dark:text-orange-200">
New
</span>