Reglas de estilo
18. Tailwind CSS
Regla
18.1: Se debe configurar Tailwind CSS específicamente para Next.js App Router con purging apropiado y optimizaciones de producción para maximizar el rendimiento.
18.2: Se deben crear componentes reutilizables que encapsulen combinaciones complejas de clases de utilidad, evitando repetición excesiva de clases.
18.3: Se debe utilizar la estructura de capas de Tailwind (@layer base, @layer components, @layer utilities) para personalizar estilos de forma ordenada.
18.4: Se debe implementar diseño responsive usando los breakpoints de Tailwind de forma consistente y mobile-first.
18.5: Se debe configurar y utilizar el sistema de dark mode de Tailwind cuando sea requerido por el proyecto.
18.6: Se deben aplicar clases de Tailwind tanto en Server Components como Client Components sin restricciones, manteniendo la consistencia.
18.7: Se debe optimizar el bundle final utilizando JIT (Just-In-Time), purging y custom utilities solo cuando sea necesario.
Contexto/Justificación
Tailwind CSS es especialmente efectivo en Next.js por su enfoque utility-first que se alinea bien con el desarrollo basado en componentes. La configuración apropiada es crucial para aprovechar las optimizaciones de Next.js como tree-shaking y code splitting. Crear componentes que encapsulen clases complejas mejora la mantenibilidad y evita la repetición de código. El uso correcto de las características de Tailwind (responsive, dark mode, custom properties) es esencial para crear interfaces modernas y accesibles.
Ejemplos
Correcto (Configuración completa):
// 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: {
colors: {
brand: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a',
}
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
animation: {
'fade-in': 'fadeIn 0.5s ease-in-out',
'slide-up': 'slideUp 0.3s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
darkMode: 'class', // o 'media' según preferencia
}
// app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
@apply scroll-smooth;
}
body {
@apply bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100;
}
h1, h2, h3, h4, h5, h6 {
@apply font-semibold text-gray-900 dark:text-gray-100;
}
}
@layer components {
.btn-primary {
@apply bg-brand-500 hover:bg-brand-600 text-white font-medium py-2 px-4 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2;
}
.btn-secondary {
@apply bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-gray-100 font-medium py-2 px-4 rounded-lg transition-colors duration-200;
}
.card {
@apply bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm;
}
.input-field {
@apply w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-brand-500 focus:border-transparent;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
.container-responsive {
@apply container mx-auto px-4 sm:px-6 lg:px-8;
}
}
Correcto (Componentes reutilizables):
// lib/utils.ts (Utility para combinar clases)
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// components/ui/Button.tsx
import { cn } from '@/lib/utils';
import { ButtonHTMLAttributes, forwardRef } from 'react';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost' | 'destructive';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', size = 'md', isLoading, children, className, ...props }, ref) => {
const variants = {
primary: 'bg-brand-500 hover:bg-brand-600 text-white',
secondary: 'bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-gray-100',
ghost: 'hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300',
destructive: 'bg-red-500 hover:bg-red-600 text-white'
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg'
};
return (
<button
ref={ref}
className={cn(
'font-medium rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed',
variants[variant],
sizes[size],
variant === 'primary' && 'focus:ring-brand-500',
variant === 'destructive' && 'focus:ring-red-500',
isLoading && 'cursor-wait',
className
)}
disabled={isLoading || props.disabled}
{...props}
>
{isLoading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
Cargando...
</div>
) : children}
</button>
);
}
);
Button.displayName = 'Button';
// components/ProductCard.tsx (Server Component con Tailwind)
interface ProductCardProps {
title: string;
price: number;
image: string;
inStock: boolean;
featured?: boolean;
}
export function ProductCard({ title, price, image, inStock, featured }: ProductCardProps) {
return (
<div className={cn(
'card p-6 group cursor-pointer transition-all duration-200',
featured && 'ring-2 ring-brand-500'
)}>
<div className="aspect-square mb-4 overflow-hidden rounded-lg bg-gray-100 dark:bg-gray-800">
<img
src={image}
alt={title}
className="h-full w-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 line-clamp-2">
{title}
</h3>
<div className="flex items-center justify-between">
<span className="text-xl font-bold text-brand-600 dark:text-brand-400">
${price.toFixed(2)}
</span>
<div className={cn(
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
inStock
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
)}>
{inStock ? 'En stock' : 'Agotado'}
</div>
</div>
{featured && (
<div className="pt-2">
<span className="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-brand-100 text-brand-800 dark:bg-brand-900 dark:text-brand-200">
⭐ Destacado
</span>
</div>
)}
</div>
</div>
);
}
Incorrecto:
// ❌ Repetir clases complejas sin crear componentes
export function ProductCard() {
return (
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6 hover:shadow-md transition-shadow duration-200">
<button className="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
Botón 1
</button>
<button className="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
Botón 2 {/* Repetición de clases */}
</button>
</div>
);
}
// ❌ No usar mobile-first approach
<div className="lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1">
{/* Orden incorrecto de breakpoints */}
</div>
// ❌ Usar estilos inline mezclados con Tailwind
<div
className="p-4 bg-white"
style={{ marginTop: '20px', color: 'blue' }} // Inconsistente con Tailwind
>
Contenido
</div>
// ❌ No aprovechar el sistema de dark mode
<div className="bg-white text-black"> {/* Sin soporte dark mode */}
Contenido
</div>
Cuándo aplicar
- Cuando se elija Tailwind CSS como sistema de estilos principal del proyecto, especialmente en aplicaciones con muchos componentes reutilizables y que requieran diseño responsive consistente.
- En proyectos que necesiten un sistema de diseño coherente y mantenible.
- Cuando el equipo esté familiarizado con utility-first CSS y las ventajas del enfoque de Tailwind.
Cuándo evitar
- En proyectos muy pequeños donde la configuración de Tailwind pueda ser excesiva.
- Cuando el equipo no esté familiarizado con utility-first CSS y prefiera enfoques tradicionales.
- Cuando se requieran estilos muy específicos que no se alineen bien con el sistema de diseño de Tailwind.
- Si ya existe una base de CSS considerable que sería difícil de migrar.
19. Configuración avanzada y herramientas de desarrollo
Regla
19.1: Se debe configurar Tailwind CSS para optimización máxima en producción, incluyendo purging correcto, JIT mode y plugins necesarios. 19.2: Se debe utilizar el sistema de tokens de diseño de Tailwind (colors, spacing, typography) de forma consistente en lugar de valores arbitrarios. 19.3: Se deben crear utilidades personalizadas solo cuando sea absolutamente necesario y no exista una alternativa en el core de Tailwind. 19.4: Se debe integrar Tailwind con herramientas de desarrollo como IntelliSense, Prettier plugin y linting específico.
Contexto/Justificación
La configuración avanzada de Tailwind permite aprovechar al máximo sus beneficios de rendimiento y experiencia de desarrollo. El uso consistente del sistema de tokens asegura coherencia visual y facilita el mantenimiento. Las herramientas de desarrollo mejoran significativamente la productividad y previenen errores comunes.
Ejemplos
Correcto (Configuración de desarrollo):
// package.json dependencies
{
"dependencies": {
"tailwindcss": "^3.4.0",
"clsx": "^2.0.0",
"tailwind-merge": "^2.0.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"prettier": "^3.0.0",
"prettier-plugin-tailwindcss": "^0.5.7"
}
}
// .prettierrc
{
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindFunctions": ["clsx", "cn"]
}
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Cuándo aplicar
- En todos los proyectos que utilicen Tailwind CSS para maximizar los beneficios de rendimiento y experiencia de desarrollo.
Cuándo evitar
- La configuración básica es suficiente para proyectos simples, pero las optimizaciones siempre son recomendables.