Tailwind CSS con React
Tailwind CSS funciona excepcionalmente bien con React. Aquí tienes buenas prácticas y patrones para usarlos juntos de forma efectiva.
Configuración con Next.js
# Crear app de Next.js con Tailwind
npx create-next-app@latest my-app --tailwind
# O agregar a un proyecto existente
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Configuración con Vite
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Clases Condicionales con clsx/cn
npm install clsx tailwind-merge
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
import { cn } from '@/lib/utils';
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
children: React.ReactNode;
}
function Button({ variant = 'primary', size = 'md', disabled, children }: ButtonProps) {
return (
<button
disabled={disabled}
className={cn(
// Estilos base
'font-semibold rounded-lg transition-colors',
// Variantes de tamaño
{
'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',
},
// Variantes de color
{
'bg-blue-600 hover:bg-blue-700 text-white': variant === 'primary',
'bg-gray-200 hover:bg-gray-300 text-gray-800': variant === 'secondary',
'border-2 border-blue-600 text-blue-600 hover:bg-blue-50': variant === 'outline',
},
// Estado disabled
disabled && 'opacity-50 cursor-not-allowed'
)}
>
{children}
</button>
);
}
Crear Componentes Reutilizables
// components/Card.tsx
import { cn } from '@/lib/utils';
interface CardProps {
className?: string;
children: React.ReactNode;
}
export function Card({ className, children }: CardProps) {
return (
<div className={cn('bg-white rounded-xl shadow-lg p-6', className)}>
{children}
</div>
);
}
export function CardHeader({ className, children }: CardProps) {
return (
<div className={cn('mb-4', className)}>
{children}
</div>
);
}
export function CardTitle({ className, children }: CardProps) {
return (
<h3 className={cn('text-xl font-bold text-gray-900', className)}>
{children}
</h3>
);
}
export function CardContent({ className, children }: CardProps) {
return (
<div className={cn('text-gray-600', className)}>
{children}
</div>
);
}
// Uso
<Card className="max-w-md">
<CardHeader>
<CardTitle>Hello World</CardTitle>
</CardHeader>
<CardContent>
Card content here
</CardContent>
</Card>
Componentes de Formulario
// components/Input.tsx
import { forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, className, ...props }, ref) => {
return (
<div className="w-full">
{label && (
<label className="block text-sm font-medium text-gray-700 mb-1">
{label}
</label>
)}
<input
ref={ref}
className={cn(
'w-full px-4 py-2 border rounded-lg outline-none transition-all',
'focus:ring-2 focus:ring-blue-500 focus:border-blue-500',
error
? 'border-red-500 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300',
className
)}
{...props}
/>
{error && (
<p className="mt-1 text-sm text-red-500">{error}</p>
)}
</div>
);
}
);
Input.displayName = 'Input';
Usando con React Hook Form
import { useForm } from 'react-hook-form';
import { Input } from './Input';
function ContactForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 max-w-md">
<Input
label="Name"
{...register('name', { required: 'Name is required' })}
error={errors.name?.message}
/>
<Input
label="Email"
type="email"
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address'
}
})}
error={errors.email?.message}
/>
<button
type="submit"
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-colors"
>
Submit
</button>
</form>
);
}
Estilos Dinámicos según Props
interface BadgeProps {
status: 'success' | 'warning' | 'error' | 'info';
children: React.ReactNode;
}
const statusStyles = {
success: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
error: 'bg-red-100 text-red-800',
info: 'bg-blue-100 text-blue-800',
};
function Badge({ status, children }: BadgeProps) {
return (
<span className={cn(
'px-2 py-1 text-xs font-semibold rounded-full',
statusStyles[status]
)}>
{children}
</span>
);
}
Componentes Responsivos
function ResponsiveGrid({ children }: { children: React.ReactNode }) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 md:gap-6">
{children}
</div>
);
}
function ResponsiveNav() {
const [isOpen, setIsOpen] = useState(false);
return (
<nav className="bg-white shadow">
<div className="max-w-7xl mx-auto px-4">
<div className="flex justify-between h-16">
<div className="flex items-center">
<span className="font-bold text-xl">Logo</span>
</div>
{/* Desktop nav */}
<div className="hidden md:flex items-center space-x-8">
<a href="#" className="text-gray-600 hover:text-gray-900">Home</a>
<a href="#" className="text-gray-600 hover:text-gray-900">About</a>
</div>
{/* Mobile menu button */}
<button
className="md:hidden p-2"
onClick={() => setIsOpen(!isOpen)}
>
Menu
</button>
</div>
{/* Mobile nav */}
{isOpen && (
<div className="md:hidden py-4 space-y-2">
<a href="#" className="block px-4 py-2 text-gray-600">Home</a>
<a href="#" className="block px-4 py-2 text-gray-600">About</a>
</div>
)}
</div>
</nav>
);
}
Animación con Tailwind y React
import { useState } from 'react';
function AnimatedCard() {
const [isHovered, setIsHovered] = useState(false);
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={cn(
'bg-white rounded-xl p-6 transition-all duration-300',
isHovered ? 'shadow-2xl scale-105' : 'shadow-lg'
)}
>
<h3 className="text-xl font-bold">Animated Card</h3>
<p className={cn(
'text-gray-600 transition-opacity duration-300',
isHovered ? 'opacity-100' : 'opacity-70'
)}>
Hover to see animation
</p>
</div>
);
}
Buenas Prácticas
- Usa el helper cn() para clases condicionales y merging
- Extrae patrones repetidos en componentes reutilizables
- Usa TypeScript para mejor validación de props
- Mantén las listas de clases organizadas - agrupa utilidades relacionadas
- Aprovecha forwardRef para componentes de formularios
- Usa variables CSS para theming dinámico