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 azultext-white
→ texto blancopx-5
→ padding horizontalpy-2
→ padding verticalrounded
→ 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
= 01
= 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:
→ 640pxmd:
→ 768pxlg:
→ 1024pxxl:
→ 1280px2xl:
→ 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>
)
}
Navbar responsive
'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. 🎨