Tailwind CSS: por qué lo usamos en todos nuestros proyectos
Tailwind CSS: por qué lo usamos en todos nuestros proyectos
Hace tres años, migrar nuestro stack de frontend a Tailwind CSS fue una de las decisiones técnicas más discutidas en el equipo. Hoy, después de más de veinte proyectos productivos, puedo afirmar con certeza que no miramos atrás. Este artículo explica los motivos técnicos que fundamentan esa elección, con ejemplos concretos de código y las lecciones que aprendimos en el camino.
El problema con CSS tradicional que no nos contaron en la universidad
Antes de Tailwind, nuestro flujo de trabajo típico involucraba uno de estos patrones: archivos CSS monolíticos que crecían descontroladamente, CSS modules que multiplicaban la complejidad cognitiva, o librerías de componentes que limitaban la personalización visual. Cualquier desarrollador con experiencia reconoce la frustración de context-switching continuo entre el markup y el stylesheet, especialmente en iteraciones rápidas de diseño.
Consideremos un componente de botón que antes escribíamos así:
/* Button.module.css */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
transition: all 0.2s;
}
.btnPrimary {
composes: btn;
background-color: #3b82f6;
color: white;
}
.btnPrimary:hover {
background-color: #2563eb;
}
.btnSecondary {
composes: btn;
background-color: #e5e7eb;
color: #1f2937;
}
/* Y luego en el JSX... */
import styles from './Button.module.css';
<button className={styles.btnPrimary}>Enviar</button>
Este enfoque presenta problemas reales: naming conventions que consumen energía mental, archivos separados que fragmentan el contexto, y la inevitable especialización de clases que lleva a .btnPrimaryLargeDisabled o peor aún, a modificadores de BEM que explotan en complejidad.
La solución de utility-first: escribir CSS sin escribir CSS
Tailwind inverte la lógica. En lugar de escribir reglas CSS que luego aplicamos a elementos, componemos estilos directamente en el markup usando utilidades atómicas. El mismo botón en Tailwind:
<button class="inline-flex items-center justify-center px-4 py-2 rounded-md font-medium transition-colors bg-blue-500 text-white hover:bg-blue-600">
Enviar
</button>
La primera reacción es habitualmente negativa: "¿HTML con clases imposibles de leer? ¿Estamos volviendo a inline styles?" La respuesta corta es no, y la explicación requiere entender lo que Tailwind resuelve versus lo que preserva.
Las utilidades de Tailwind no son inline styles porque mantienen las ventajas fundamentales de CSS: media queries, pseudo-clases, combinaciones con selectores hijos, y fundamentalmente, un sistema de diseño consistente mediante valores predefinidos. No escribimos font-size: 14px arbitrario; usamos text-sm que mapea a 0.875rem (14px) y se mantiene coherente en toda la aplicación.
El sistema de diseño implícito que funciona
La disciplina más valiosa que Tailwind impone es un sistema de diseño restrictivo. Los valores arbitrarios existen (text-[13px]), pero su sintaxis explícita los hace conscientes y auditeables. Comparen dos aproximaciones:
/* Enfoque sin restricciones: el caos crece silenciosamente */
.card {
margin-bottom: 18px;
padding: 22px 16px;
border-radius: 7px;
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
}
/* Otro archivo, otro desarrollador */
.feature-box {
margin-bottom: 20px;
padding: 24px 18px;
border-radius: 8px;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
}
En Tailwind, las decisiones de diseño se canalizan:
<div class="mb-4 p-5 px-4 rounded-lg shadow-md"></div>
<!-- En otro componente, el mismo desarrollador u otro -->
<div class="mb-4 p-6 px-4 rounded-lg shadow-lg"></div>
La diferencia es sutil pero crítica: los valores provienen de una escala predefinida (mb-4 siempre es 1rem, rounded-lg siempre es 0.5rem). La consistencia emerge del sistema, no de la disciplina individual.
Configuración del tema que adaptamos proyecto a proyecto
La escalabilidad de esta restricción depende de tailwind.config.js, donde definimos las variables de nuestro diseño:
module.exports = {
theme: {
extend: {
colors: {
brand: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
900: '#1e3a8a',
}
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
spacing: {
'18': '4.5rem',
'88': '22rem',
}
}
}
}
Esta configuración centralizada elimina la duplicación de tokens de diseño en variables CSS dispersas o peor, hardcodeadas en decenas de archivos. Cuando el equipo de producto decide que el azul principal es ligeramente más oscuro, cambiamos un valor en un archivo y rebuild.
Productividad medible en componentes React
Nuestro stack principal es React, donde Tailwind demuestra su máxima eficiencia. La co-localización de estilos y lógica de UI reduce drásticamente el tiempo de navegación entre archivos. Un componente real de nuestro último proyecto:
import { useState } from 'react';
export function UserCard({ user, onSelect, isSelected }) {
const [imageLoaded, setImageLoaded] = useState(false);
return (
<article
onClick={onSelect}
className={`
relative flex items-center gap-4 p-4 rounded-xl cursor-pointer
transition-all duration-200 border-2
${isSelected
? 'border-brand-500 bg-brand-50 shadow-md'
: 'border-transparent bg-white hover:border-gray-200 shadow-sm hover:shadow-md'
}
`}
>
<div className="relative w-12 h-12 shrink-0">
{!imageLoaded && (
<div className="absolute inset-0 rounded-full bg-gray-200 animate-pulse" />
)}
<img
src={user.avatar}
alt={user.name}
onLoad={() => setImageLoaded(true)}
className={`
w-full h-full rounded-full object-cover
transition-opacity duration-300
${imageLoaded ? 'opacity-100' : 'opacity-0'}
`}
/>
{user.isOnline && (
<span className="absolute bottom-0 right-0 w-3 h-3 bg-green-500 border-2 border-white rounded-full" />
)}
</div>
<div className="min-w-0 flex-1">
<h3 className="font-semibold text-gray-900 truncate">{user.name}</h3>
<p className="text-sm text-gray-500 truncate">{user.role}</p>
</div>
<span className={`
px-2 py-1 text-xs font-medium rounded-full
${user.status === 'active'
? 'bg-green-100 text-green-700'
: 'bg-gray-100 text-gray-600'
}
`}>
{user.status}
</span>
</article>
);
}
Este componente, en nuestro flujo anterior con styled-components, habría requerido entre tres y cinco archivos (componente, estilos, theme tokens, posiblemente animaciones separadas) y aproximadamente el doble de líneas de código. La versión con Tailwind es autónoma, legible para cualquier desarrollador familiarizado con el framework, y trivial de modificar sin romper otros componentes.
Responsividad sin media queries manuales
Uno de los beneficios más subestimados es el sistema de breakpoints profundamente integrado. En CSS tradicional o incluso con librerías CSS-in-JS, las media queries añaden verbosity significativa. En Tailwind:
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 md:gap-6 p-4 md:p-8">
{items.map(item => <Card key={item.id} {...item} />)}
</div>
Los prefijos sm:, md:, lg:, xl: aplican la utilidad únicamente en ese breakpoint y superiores (mobile-first por diseño). No hay que recordar sintaxis de media queries, no hay que nombrar breakpoints, no hay que preocuparse por la especificidad. La responsividad se vuelve una dimensión más del markup, no una preocupación transversal.
La crítica recurrente: ¿y la repetición de clases?
El argumento más válido contra Tailwind es la duplicación de utilidades en múltiples componentes similares. Nuestra respuesta tiene dos componentes técnicos: @apply para casos excepcionales, y principalmente, la extracción de componentes de React.
Cuando detectamos patrones repetidos, no creamos clases CSS abstractas. Creamos componentes React:
// No hacemos esto salvo excepciones justificadas
// .btn-primary { @apply px-4 py-2 bg-blue-500 ... }
// Hacemos esto
function Button({ variant = 'primary', size = 'md', children, ...props }) {
const base = 'inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2';
const variants = {
primary: 'bg-brand-600 text-white hover:bg-brand-700 focus:ring-brand-500',
secondary: 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50 focus:ring-brand-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base',
};
return (
<button
className={`${base} ${variants[variant]} ${sizes[size]}`}
{...props}
>
{children}
</button>
);
}
Esta abstracción preserva las ventajas de Tailwind (sistema de diseño, no-naming de clases, co-localización) mientras elimina la repetición. Es más poderosa que una clase CSS porque puede incorporar lógica de props, estados y comportamiento.
Optimización de build: CSS que no escala con la aplicación
Un aspecto técnico decisivo en nuestra adopción fue el tamaño del CSS en producción. Tailwind genera un CSS inicial enorme en desarrollo (todas las utilidades posibles), pero en producción, PurgeCSS (ahora JIT mode) elimina lo no utilizado.
En nuestros proyectos, el CSS final típico oscila entre 8KB y 15KB comprimido, independientemente del tamaño de la aplicación. Comparado con 200KB+ de librerías de componentes completas o CSS críticos crecientes en proyectos legacy, la diferencia en métricas de performance es mensurable y significativa.
El JIT mode, activo por defecto desde v3, compila las utilidades bajo demanda:
// tailwind.config.js
module.exports = {
content: [
'./src/**/*.{js,jsx,ts,tsx}',
'./public/index.html',
],
// Sin 'purge' necesario, el JIT se encarga automáticamente
}
Integración con el ecosistema React moderno
La experiencia de desarrollo se completa con herramientas que mitigan la única fricción real: la verbosidad del markup. Usamos prettier-plugin-tailwindcss para ordenación consistente de clases, y tailwind-merge cuando los componentes aceptan className overrides:
import { twMerge } from 'tailwind-merge';
function Alert({ variant, className, children }) {
const base = 'p-4 rounded-lg border';
const styles = {
info: 'bg-blue-50 border-blue-200 text-blue-800',
warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
error: 'bg-red-50 border-red-200 text-red-800',
};
return (
<div className={twMerge(base, styles[variant], className)}>
{children}
</div>
);
}
twMerge resuelve inteligentemente conflictos: si el consumidor pasa className="bg-green-50", sobrescribe el bg-blue-50 sin duplicar propiedades incompatibles ni requerir sintaxis de objeto compleja.
Cuándo no usamos Tailwind
Sería deshonesto afirmar que es universal. Evitamos Tailwind en: proyectos con requisitos de animación compleja donde CSS puro o GSAP son superiores; colaboraciones con diseñadores que trabajan exclusivamente en Figma sin familiaridad con sistemas de utilidades; y ocasionalmente, para landing pages altamente artísticas donde cada sección es un diseño único sin componentes repetidos.
Incluso en estos casos, a menudo usamos Tailwind como base y extendemos con CSS modules para las excepciones, en lugar de abandonarlo completamente.
Conclusión práctica
Tres años de evidencia consolidan nuestra decisión. Tailwind CSS no es perfecto, pero optimiza exactamente las variables que importan en desarrollo frontend comercial: velocidad de iteración, consistencia de diseño sin sobrecarga de coordinación, tamaño de bundle predecible, y curva de onboarding planar para desarrolladores que ya conocen CSS.
La resistencia inicial al paradigma utility-first es real y válida. Nuestro equipo la experimentó colectivamente. La superamos cuando medimos el tiempo de implementación de features comparables en proyectos paralelos, y los resultados fueron inequívocos: 30-40% menos tiempo en implementación de UI, especialmente en fases de refinamiento visual con el equipo de diseño.
La pregunta correcta no es si Tailwind es "mejor CSS". Es si el trade-off de verbosidad en markup compensa la eliminación de problemas sistémicos que conocemos demasiado bien. Para nosotros, en el contexto de aplicaciones React productivas, la respuesta se afirma con cada proyecto que entregamos.