Tailwind CSS

Tailwind CSS es un framework de CSS basado en clases utilitarias. En lugar de escribir CSS personalizado, usas clases predefinidas directamente en tu HTML.

¿Qué son clases utilitarias?

En lugar de esto:

/* styles.css */
.button {
  background-color: blue;
  color: white;
  padding: 10px 20px;
  border-radius: 5px;
}
<button className="button">Click</button>

Haces esto:

<button className="bg-blue-500 text-white px-5 py-2 rounded">
  Click
</button>

Cada clase hace una cosa específica:

  • bg-blue-500 → fondo azul
  • text-white → texto blanco
  • px-5 → padding horizontal
  • py-2 → padding vertical
  • rounded → bordes redondeados

Instalación

NextJS viene con soporte para Tailwind incluido:

npx create-next-app@latest mi-proyecto
# Selecciona "Yes" cuando pregunte por Tailwind CSS

Instalación manual

Si tu proyecto ya existe:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Esto crea dos archivos:

tailwind.config.js:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

postcss.config.js:

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

app/globals.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

app/layout.tsx:

import './globals.css'

export default function RootLayout({ children }) {
  return (
    <html lang="es">
      <body>{children}</body>
    </html>
  )
}

Listo, ya puedes usar Tailwind en cualquier componente.

Conceptos básicos

Espaciado

Tailwind usa una escala numérica para espaciado:

<div className="p-4">        {/* padding: 1rem (16px) */}
<div className="m-8">        {/* margin: 2rem (32px) */}
<div className="px-6">       {/* padding horizontal: 1.5rem */}
<div className="py-2">       {/* padding vertical: 0.5rem */}
<div className="mt-10">      {/* margin-top: 2.5rem */}
<div className="mb-4">       {/* margin-bottom: 1rem */}

Escala completa:

  • 0 = 0
  • 1 = 0.25rem (4px)
  • 2 = 0.5rem (8px)
  • 4 = 1rem (16px)
  • 8 = 2rem (32px)
  • 12 = 3rem (48px)
  • 16 = 4rem (64px)

Colores

Sistema de colores con niveles del 50 al 950:

<div className="bg-blue-500">     {/* Fondo azul medio */}
<div className="bg-blue-900">     {/* Fondo azul oscuro */}
<div className="text-red-600">    {/* Texto rojo */}
<div className="border-green-500"> {/* Borde verde */}

Colores disponibles: slate, gray, zinc, neutral, stone, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose

Tipografía

<h1 className="text-3xl font-bold">         {/* Grande y negrita */}
<p className="text-base font-normal">       {/* Tamaño normal */}
<span className="text-sm text-gray-600">    {/* Pequeño y gris */}
<p className="italic underline">            {/* Cursiva y subrayado */}
<p className="text-center">                 {/* Centrado */}
<p className="leading-relaxed">             {/* Espaciado entre líneas */}

Tamaños: text-xs, text-sm, text-base, text-lg, text-xl, text-2xl, text-3xl, text-4xl, text-5xl

Flexbox y Grid

{/* Flexbox */}
<div className="flex items-center justify-between">
  <span>Logo</span>
  <nav>Menu</nav>
</div>

{/* Grid */}
<div className="grid grid-cols-3 gap-4">
  <div>Item 1</div>
  <div>Item 2</div>
  <div>Item 3</div>
</div>

{/* Flex con dirección */}
<div className="flex flex-col space-y-4">
  <div>Item 1</div>
  <div>Item 2</div>
</div>

Responsive Design

Tailwind es mobile-first. Sin prefijo = móvil, con prefijo = desktop:

<div className="
  w-full          {/* Ancho completo en móvil */}
  md:w-1/2        {/* 50% de ancho en tablet */}
  lg:w-1/3        {/* 33% de ancho en desktop */}
">
  Responsive
</div>

Breakpoints:

  • sm: → 640px
  • md: → 768px
  • lg: → 1024px
  • xl: → 1280px
  • 2xl: → 1536px

Ejemplos prácticos

Botón básico

export default function Button({ children, variant = 'primary' }) {
  const styles = {
    primary: 'bg-blue-500 hover:bg-blue-600 text-white',
    secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
    outline: 'border-2 border-blue-500 text-blue-500 hover:bg-blue-50',
  }
  
  return (
    <button className={`
      px-6 py-2 rounded-lg font-semibold
      transition-colors duration-200
      ${styles[variant]}
    `}>
      {children}
    </button>
  )
}

Uso:

<Button variant="primary">Guardar</Button>
<Button variant="secondary">Cancelar</Button>
<Button variant="outline">Más info</Button>

Card de producto

export default function ProductoCard({ producto }) {
  return (
    <div className="
      border rounded-lg overflow-hidden
      hover:shadow-lg transition-shadow duration-300
      bg-white
    ">
      {/* Imagen */}
      <div className="relative h-48 bg-gray-200">
        <img 
          src={producto.imagen} 
          alt={producto.nombre}
          className="w-full h-full object-cover"
        />
        {producto.descuento && (
          <span className="
            absolute top-2 right-2
            bg-red-500 text-white
            px-2 py-1 rounded text-sm font-bold
          ">
            -{producto.descuento}%
          </span>
        )}
      </div>
      
      {/* Contenido */}
      <div className="p-4">
        <h3 className="text-lg font-semibold text-gray-900 mb-2">
          {producto.nombre}
        </h3>
        
        <p className="text-gray-600 text-sm mb-4">
          {producto.descripcion}
        </p>
        
        <div className="flex items-center justify-between">
          <span className="text-2xl font-bold text-blue-600">
            ${producto.precio}
          </span>
          
          <button className="
            bg-blue-500 hover:bg-blue-600
            text-white px-4 py-2 rounded
            transition-colors
          ">
            Agregar
          </button>
        </div>
      </div>
    </div>
  )
}

Grid de productos

export default function ProductosGrid({ productos }) {
  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">Nuestros Productos</h1>
      
      <div className="
        grid 
        grid-cols-1 
        sm:grid-cols-2 
        lg:grid-cols-3 
        xl:grid-cols-4 
        gap-6
      ">
        {productos.map(producto => (
          <ProductoCard key={producto.id} producto={producto} />
        ))}
      </div>
    </div>
  )
}

Formulario

export default function FormularioContacto() {
  return (
    <form className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-lg">
      <h2 className="text-2xl font-bold mb-6 text-gray-900">
        Contáctanos
      </h2>
      
      {/* Input de texto */}
      <div className="mb-4">
        <label className="block text-gray-700 font-semibold mb-2">
          Nombre
        </label>
        <input 
          type="text"
          className="
            w-full px-4 py-2 border border-gray-300 rounded-lg
            focus:outline-none focus:ring-2 focus:ring-blue-500
            transition-all
          "
          placeholder="Tu nombre"
        />
      </div>
      
      {/* Email */}
      <div className="mb-4">
        <label className="block text-gray-700 font-semibold mb-2">
          Email
        </label>
        <input 
          type="email"
          className="
            w-full px-4 py-2 border border-gray-300 rounded-lg
            focus:outline-none focus:ring-2 focus:ring-blue-500
          "
          placeholder="tu@email.com"
        />
      </div>
      
      {/* Textarea */}
      <div className="mb-6">
        <label className="block text-gray-700 font-semibold mb-2">
          Mensaje
        </label>
        <textarea 
          rows={4}
          className="
            w-full px-4 py-2 border border-gray-300 rounded-lg
            focus:outline-none focus:ring-2 focus:ring-blue-500
            resize-none
          "
          placeholder="Tu mensaje..."
        />
      </div>
      
      {/* Botón */}
      <button className="
        w-full bg-blue-500 hover:bg-blue-600 
        text-white font-bold py-3 rounded-lg
        transition-colors
      ">
        Enviar
      </button>
    </form>
  )
}
'use client'

import { useState } from 'react'
import Link from 'next/link'

export default function Navbar() {
  const [isOpen, setIsOpen] = useState(false)
  
  return (
    <nav className="bg-white shadow-lg">
      <div className="container mx-auto px-4">
        <div className="flex justify-between items-center h-16">
          {/* Logo */}
          <Link href="/" className="text-2xl font-bold text-blue-600">
            MiTienda
          </Link>
          
          {/* Desktop Menu */}
          <div className="hidden md:flex space-x-8">
            <Link href="/productos" className="text-gray-700 hover:text-blue-600">
              Productos
            </Link>
            <Link href="/ofertas" className="text-gray-700 hover:text-blue-600">
              Ofertas
            </Link>
            <Link href="/contacto" className="text-gray-700 hover:text-blue-600">
              Contacto
            </Link>
          </div>
          
          {/* Botón móvil */}
          <button 
            onClick={() => setIsOpen(!isOpen)}
            className="md:hidden"
          >
            <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
            </svg>
          </button>
        </div>
        
        {/* Mobile Menu */}
        {isOpen && (
          <div className="md:hidden pb-4">
            <Link 
              href="/productos" 
              className="block py-2 text-gray-700 hover:text-blue-600"
            >
              Productos
            </Link>
            <Link 
              href="/ofertas" 
              className="block py-2 text-gray-700 hover:text-blue-600"
            >
              Ofertas
            </Link>
            <Link 
              href="/contacto" 
              className="block py-2 text-gray-700 hover:text-blue-600"
            >
              Contacto
            </Link>
          </div>
        )}
      </div>
    </nav>
  )
}

Estados interactivos

Hover, Focus, Active

<button className="
  bg-blue-500 
  hover:bg-blue-600      {/* Al pasar el mouse */}
  active:bg-blue-700     {/* Al hacer click */}
  focus:ring-4           {/* Al enfocar */}
  focus:ring-blue-300
">
  Botón
</button>

<input className="
  border border-gray-300
  focus:border-blue-500  {/* Al enfocar */}
  focus:outline-none
  focus:ring-2
  focus:ring-blue-200
" />

Disabled

<button 
  disabled
  className="
    bg-blue-500 text-white px-4 py-2 rounded
    disabled:bg-gray-300      {/* Cuando está disabled */}
    disabled:cursor-not-allowed
  "
>
  No disponible
</button>

Group Hover

Cambia un elemento hijo cuando el padre tiene hover:

<div className="group cursor-pointer">
  <div className="bg-blue-500 group-hover:bg-blue-600">
    Tarjeta
  </div>
  <p className="text-gray-600 group-hover:text-blue-600">
    Este texto cambia cuando haces hover en la tarjeta
  </p>
</div>

Personalización

Colores personalizados

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        'brand': {
          50: '#f0f9ff',
          100: '#e0f2fe',
          500: '#0ea5e9',
          900: '#0c4a6e',
        },
      },
    },
  },
}

Uso:

<div className="bg-brand-500 text-brand-50">
  Color personalizado
</div>

Fuentes personalizadas

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      fontFamily: {
        sans: ['Inter', 'sans-serif'],
        heading: ['Poppins', 'sans-serif'],
      },
    },
  },
}

Uso:

<h1 className="font-heading text-4xl">Título</h1>
<p className="font-sans">Párrafo</p>

Espaciado personalizado

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      spacing: {
        '128': '32rem',
        '144': '36rem',
      },
    },
  },
}

Uso:

<div className="h-128 w-144">
  Tamaño personalizado
</div>

Directivas de Tailwind

@apply

Extrae clases repetitivas a CSS:

/* globals.css */
@layer components {
  .btn-primary {
    @apply px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors;
  }
  
  .card {
    @apply bg-white rounded-lg shadow-lg p-6;
  }
}

Uso:

<button className="btn-primary">Click</button>
<div className="card">Contenido</div>
⚠️
Usa @apply con moderación

El poder de Tailwind está en usar las clases directamente. Solo usa @apply para componentes que repites muchas veces y que son complejos.

@layer

Organiza tu CSS en capas:

@layer base {
  h1 {
    @apply text-4xl font-bold;
  }
  
  h2 {
    @apply text-3xl font-semibold;
  }
}

@layer components {
  .btn {
    @apply px-4 py-2 rounded;
  }
}

@layer utilities {
  .text-shadow {
    text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
  }
}

Plugins útiles

Tailwind Forms

Mejora el estilo de formularios:

npm install @tailwindcss/forms
// tailwind.config.js
module.exports = {
  plugins: [
    require('@tailwindcss/forms'),
  ],
}

Tailwind Typography

Para contenido de blog/markdown:

npm install @tailwindcss/typography
<article className="prose lg:prose-xl">
  {/* Tu contenido HTML/Markdown aquí se estiliza automáticamente */}
  <h1>Título</h1>
  <p>Párrafo...</p>
</article>

Tailwind Container Queries

Para componentes responsive basados en su contenedor:

npm install @tailwindcss/container-queries
<div className="@container">
  <div className="@lg:flex @lg:gap-4">
    {/* Flex solo cuando el contenedor es grande */}
  </div>
</div>

Optimización

Purge automático

Tailwind elimina automáticamente CSS no usado en producción:

// tailwind.config.js
module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  // Tailwind escanea estos archivos y solo incluye las clases que usas
}

Resultado:

  • Desarrollo: ~3-4 MB (todas las clases)
  • Producción: ~5-10 KB (solo las que usas)

Clases dinámicas

// ❌ Mal: Tailwind no detecta estas clases
<div className={`text-${color}-500`}>  {/* NO funciona */}

// ✅ Bien: Clases completas
const colors = {
  red: 'text-red-500',
  blue: 'text-blue-500',
}
<div className={colors[color]}>  {/* Funciona */}

// ✅ O usa la safelist

tailwind.config.js con safelist:

module.exports = {
  safelist: [
    'text-red-500',
    'text-blue-500',
    'text-green-500',
  ],
}

Patrones comunes

Componente reutilizable

// components/Button.tsx
import { ComponentProps } from 'react'

type ButtonProps = ComponentProps<'button'> & {
  variant?: 'primary' | 'secondary' | 'danger'
  size?: 'sm' | 'md' | 'lg'
}

export default function Button({ 
  variant = 'primary', 
  size = 'md',
  className = '',
  children,
  ...props 
}: ButtonProps) {
  const baseStyles = 'font-semibold rounded transition-colors'
  
  const variants = {
    primary: 'bg-blue-500 hover:bg-blue-600 text-white',
    secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
    danger: 'bg-red-500 hover:bg-red-600 text-white',
  }
  
  const sizes = {
    sm: 'px-3 py-1 text-sm',
    md: 'px-4 py-2',
    lg: 'px-6 py-3 text-lg',
  }
  
  return (
    <button 
      className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
      {...props}
    >
      {children}
    </button>
  )
}

Conditional classes con clsx

npm install clsx
import clsx from 'clsx'

export default function Button({ primary, disabled, className }) {
  return (
    <button className={clsx(
      'px-4 py-2 rounded font-semibold',
      primary ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800',
      disabled && 'opacity-50 cursor-not-allowed',
      className
    )}>
      Click
    </button>
  )
}

Dark mode

// tailwind.config.js
module.exports = {
  darkMode: 'class', // o 'media'
  // ...
}
// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="es" className="dark"> {/* Agrega clase dark */}
      <body>{children}</body>
    </html>
  )
}

Uso:

<div className="
  bg-white dark:bg-gray-900
  text-gray-900 dark:text-white
">
  Contenido
</div>

Herramientas de desarrollo

Tailwind CSS IntelliSense (VSCode)

Autocompletado de clases:

Extensión: "Tailwind CSS IntelliSense"

Prettier Plugin

Ordena clases automáticamente:

npm install -D prettier prettier-plugin-tailwindcss
// .prettierrc
{
  "plugins": ["prettier-plugin-tailwindcss"]
}

Antes:

<div className="text-white bg-blue-500 px-4 py-2 rounded">

Después:

<div className="rounded bg-blue-500 px-4 py-2 text-white">

Mejores prácticas

1. Ordena tus clases

// Orden recomendado:
// 1. Layout (display, position)
// 2. Box model (width, padding, margin)
// 3. Typography
// 4. Visual (background, border)
// 5. Effects

<div className="
  flex items-center        {/* Layout */}
  w-full p-4 mb-4         {/* Box model */}
  text-lg font-semibold   {/* Typography */}
  bg-white border rounded {/* Visual */}
  shadow-lg               {/* Effects */}
">

2. Extrae componentes, no clases

// ❌ Mal: Repetir clases
<button className="px-4 py-2 bg-blue-500 text-white rounded">Guardar</button>
<button className="px-4 py-2 bg-blue-500 text-white rounded">Enviar</button>
<button className="px-4 py-2 bg-blue-500 text-white rounded">Aceptar</button>

// ✅ Bien: Componente reutilizable
<Button>Guardar</Button>
<Button>Enviar</Button>
<Button>Aceptar</Button>

3. Usa clases semánticas cuando tenga sentido

@layer components {
  .btn-primary {
    @apply px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600;
  }
}

4. Mobile-first siempre

// ✅ Bien: Mobile primero
<div className="text-sm md:text-base lg:text-lg">

// ❌ Mal: Desktop primero
<div className="text-lg md:text-base sm:text-sm">

5. Mantén la consistencia

Usa el sistema de diseño de Tailwind:

  • Colores: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900
  • Espaciado: 0, 1, 2, 4, 6, 8, 10, 12, 16, 20, 24
  • No inventes valores arbitrarios: p-[13px]

Recursos

  • Documentación oficial: tailwindcss.com
  • Tailwind UI: Componentes premium oficiales
  • Headless UI: Componentes accesibles sin estilos
  • daisyUI: Librería de componentes para Tailwind
  • Flowbite: Componentes UI con Tailwind

Resumen

Tailwind CSS en NextJS:

  • Framework de clases utilitarias
  • Setup incluido en create-next-app
  • Purge automático en producción
  • Sistema de diseño consistente
  • Mobile-first por defecto
  • Excelente DX con IntelliSense

Ventajas principales:

  • Desarrollo rápido
  • No cambias entre archivos
  • Bundle size pequeño en producción
  • Sistema de diseño incluido
  • Comunidad enorme

Cuándo usar:

  • Proyectos nuevos
  • Desarrollo rápido
  • Equipos que valoran consistencia
  • Casi cualquier tipo de aplicación web

Tailwind es la opción más popular en 2025 por una razón: hace el desarrollo más rápido sin sacrificar performance. 🎨