TechLead
Lección 9 de 9
5 min de lectura
Tailwind CSS

Tailwind CSS con React

Buenas prácticas para usar Tailwind CSS en aplicaciones React y Next.js, incluyendo organización de componentes y estilos condicionales.

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

Continuar Aprendiendo