wandres.dev
ARQUITECTURA II · Utility-first, CSS-in-JS y el estado del arte

El modelo de Tailwind, por dentro

Cómo funciona mecánicamente un motor de utilidades: el escaneo del marcado, la generación bajo demanda, la escala como restricción y qué cambió cuando la configuración se mudó a CSS.

⏱ 17 min

Antes de discutir si Tailwind es buena idea hay que entender qué hace exactamente, porque las dos caricaturas habituales —“son estilos en línea” y “es la única forma sensata de escribir CSS”— fallan por no mirar el mecanismo. Esta lección no defiende ni ataca nada: describe la máquina. El juicio viene en la lección siguiente, y sale mucho mejor cuando sabes qué estás juzgando.

🎯 Al terminar esta lección sabrás
  • Describir el ciclo de escaneo y generación de un motor de utilidades.
  • Explicar por qué el tamaño de la salida no depende del tamaño del proyecto.
  • Entender la escala de diseño como restricción y el papel de los valores arbitrarios.
  • Situar qué cambió al mover la configuración de JavaScript a CSS.

El ciclo: escanear, generar, nada más

Un motor de utilidades no es una biblioteca de CSS que importas. Es un compilador que produce una hoja de estilos a medida de tu marcado, y el ciclo tiene tres pasos.

Primero, escanea los ficheros de tu proyecto buscando cadenas que parezcan nombres de clase. No parsea HTML ni entiende tu framework: busca secuencias de texto compatibles con su gramática de clases, en cualquier fichero que le indiques. Esto tiene una consecuencia inmediata y es la fuente del bug más frecuente del sistema: una clase construida dinámicamente no existe.

// no se genera nada: la cadena completa nunca aparece en el fichero
const clase = `text-${color}-500`;

// si se genera: las dos cadenas completas estan en el codigo
const clase = peligro ? 'text-red-500' : 'text-slate-500';

Segundo, para cada clase reconocida, la resuelve contra su gramática y genera la regla correspondiente. p-4 no está escrito en ningún fichero de la biblioteca: se deriva de un patrón —p- más un paso de la escala de espaciado— en el momento de la compilación. Por eso el conjunto de clases posibles es esencialmente infinito y el conjunto de clases generadas es exactamente el que usas.

Tercero, emite la hoja. Y aquí está la propiedad que define económicamente el modelo: el tamaño de la salida es proporcional al número de combinaciones distintas de propiedad y valor que aparecen en el proyecto, no al número de componentes. Añadir la página número doscientos usando las mismas utilidades que las anteriores añade cero bytes de CSS. Es la misma observación que hacía Atomic CSS en 2013, con la diferencia decisiva de que ahora el compilador elimina el coste de enviar lo que no usas.

ℹ️
Purga no es la palabra correcta

Se sigue diciendo que el motor “purga” el CSS que no usas, y la mecánica es la contraria: no genera nada que no haya visto. La diferencia importa porque explica el fallo de las clases dinámicas. No es que se eliminen por error; es que nunca llegaron a existir. La solución es la misma en todos los motores: escribir las cadenas completas, o declararlas explícitamente en una lista de seguridad.

La escala como restricción

La parte del modelo que menos se discute y más efecto tiene es que las utilidades no aceptan cualquier valor: aceptan pasos de una escala definida. p-4 no es “cuatro píxeles”, es “el paso cuatro de la escala de espaciado”. text-sm es un paso de la escala tipográfica. bg-slate-800 es un tono de una rampa.

Eso convierte el sistema de diseño en algo que el compilador puede hacer cumplir. En una hoja escrita a mano, evitar que aparezca padding: 13px requiere revisión humana o un linter. En un sistema de utilidades no hace falta vigilarlo: la clase no existe. Es la misma diferencia entre una convención y un mecanismo que recorría todo el nivel anterior.

La escala tiene una válvula de escape, los valores arbitrarios, con una sintaxis deliberadamente incómoda:

<div class="p-4 top-[117px] grid-cols-[1fr_auto_1fr]"></div>

El diseño de esa sintaxis es intencionado en los dos sentidos. Existe porque un sistema sin escapes obliga a abandonarlo entero en cuanto aparece un caso legítimo fuera de la escala, y es fea porque cada aparición debe ser visible en la revisión de código. Un fichero lleno de corchetes es una señal de que el proyecto se ha salido de su propio sistema, y esa señal es el mecanismo de control.

Variantes: lo que los estilos en línea no pueden hacer

La comparación con los estilos en línea se cae aquí, y conviene ser preciso sobre por qué. Un atributo style no puede expresar :hover, ni @media, ni :focus-visible, ni @container, ni ::before, ni :has(). Las utilidades sí, mediante prefijos que se componen:

<button class="bg-slate-100 hover:bg-slate-200 focus-visible:outline-2 md:px-6 dark:bg-slate-800 @lg:flex-row">

Cada prefijo envuelve la declaración generada en el contexto correspondiente. md:px-6 genera una regla dentro de una media query; @lg:flex-row genera una regla dentro de una container query; dark: genera la variante del esquema oscuro. Son cosas que un atributo style no puede hacer de ninguna forma, y por eso la equivalencia con estilos en línea es falsa a nivel mecánico, independientemente de lo que uno opine del resultado estético.

La contrapartida es igual de real: el atributo class de un elemento con cinco variantes y ocho propiedades tiene cuarenta tokens, y la legibilidad de esa línea es objetivamente mala. Ese es un coste, no una opinión, y lo que se discute es si se compensa.

Cuando la configuración se mudó a CSS

El cambio importante de las versiones recientes —la 4, publicada a comienzos de 2025— es que la configuración dejó de vivir en un fichero de JavaScript y pasó a vivir en el propio CSS:

@import "tailwindcss";

@theme {
  --color-marca-600: oklch(0.55 0.18 265);
  --spacing: 0.25rem;
  --font-display: "Inter Variable", sans-serif;
}

Esto no es cosmético y tiene tres consecuencias que cambian la conversación.

La primera es que los tokens del tema son custom properties reales, presentes en la hoja de salida. Es decir, existen en runtime, se pueden leer desde JavaScript, se pueden sobrescribir en un subárbol y se pueden consultar desde CSS escrito a mano. Con la configuración en JavaScript, esos valores se resolvían en compilación y desaparecían; ahora conviven con cualquier CSS normal que escribas al lado.

La segunda es que el motor emite su salida dentro de capas en cascada. Eso significa que tu CSS propio puede colocarse en una capa posterior y ganar sin !important, o en una anterior y perder a propósito. El conflicto clásico entre “el CSS del framework” y “mi CSS” pasa a ser una decisión declarada en lugar de una carrera de especificidad.

La tercera es que el propio motor se apoya en funciones modernas —@property, color-mix(), container queries, @starting-style— lo que sube su suelo de compatibilidad y a la vez hace que las utilidades generadas sean CSS que reconocerías si lo escribieras tú.

El motor de utilidades no compite con CSS: compite con tu capacidad de mantener una convención

La forma más clara de entender qué es realmente este modelo es fijarse en qué sustituye, y no es el lenguaje. Todo lo que genera es CSS que podrías escribir a mano en menos líneas: p-4 es una declaración. Lo que el motor sustituye es el conjunto de procesos humanos que en un proyecto tradicional garantizan que los valores salgan de la escala, que nadie invente un tono nuevo, que el CSS muerto se borre, que dos personas no escriban la misma regla con nombres distintos y que la especificidad no escale. En un equipo pequeño y disciplinado, con revisión de código atenta, esos procesos funcionan y el motor aporta poco a cambio de un marcado más ruidoso. En un equipo de cuarenta personas, con rotación, con plazos y con tres generaciones de código conviviendo, esos procesos no funcionan: es un hecho empírico bien documentado por veinte años de hojas de estilo que crecen sin parar. Ahí el motor aporta muchísimo, porque convierte disciplina en compilación. Por eso las dos posturas del debate suelen ser sinceras y a la vez irreconciliables: quien dice “no lo necesito” suele estar describiendo con exactitud su contexto, y quien dice “es imprescindible” también describe el suyo. La pregunta útil no es cuál tiene razón, sino cuánta disciplina puede sostener tu equipo real durante los próximos cinco años, y esa respuesta no es técnica.