Módulo 3: Utility First With Tailwind

Por qué utilidades y no clases semánticas

Descripción

La lección 2 te mostró cómo se estila con utilidades. Esta responde la pregunta incómoda que deja: si el estilo final es el mismo —cinco declaraciones, ya sea en una clase .product-card o en cinco utilidades—, ¿qué se gana poniéndolo en el marcado en vez de en una clase con nombre? La respuesta son cuatro ventajas concretas, y ninguna es estética: velocidad (no cambias de archivo ni inventas nombres para avanzar), no tener que nombrar todo (nombrar bien es de los problemas más difíciles de programar, y utility-first lo elimina para la mayoría de los casos), un CSS que no crece al agregar pantallas (el que más sorprende, y el que vamos a medir), y consistencia que sale de que las utilidades leen de una escala en vez de valores inventados.

Esta lección es el por qué del módulo. Sin ella, utility-first parece una moda o una preferencia de gusto. Con ella, ves que resuelve problemas reales que sufriste en web-fundamentals —el CSS que crece sin control, los nombres de clase que ya no sabes si usas, el "¿este azul es el mismo azul?"—. Vamos a hacer tangible el argumento del CSS que no crece ejecutando un modelo: cuánto CSS agrega una pantalla nueva en cada enfoque.

Conexión con el módulo. La presentación dio la tesis y la lección 2 la mecánica; esta da la justificación. Es la bisagra del módulo: después de ella, cada lección técnica (cómo funciona por debajo, la cascada, los tokens, @apply) tiene un para qué claro. En particular, el cuarto argumento —consistencia por la escala— apunta directo al módulo 4 (las escalas), y el argumento del CSS que no crece apunta a performance (Tailwind purga lo no usado, otra guía). Aquí reunimos los porqués; el resto del módulo los sostiene con mecánica.

Una analogía: el inventario de piezas custom vs la caja de ladrillos

Imagina que diriges dos talleres de casas de juguete, y comparas sus inventarios al cabo de un año.

El taller semántico fabrica una pieza custom con nombre por cada panel de cada casa: panel-frontal-casa-1, panel-lateral-casa-1, panel-frontal-casa-2… Cada casa nueva suma piezas nuevas a la estantería. Al año, la estantería tiene cientos de piezas, muchas casi idénticas (el panel-frontal de la casa 1 y el de la 12 son el mismo salvo un detalle), y nadie sabe ya cuáles se siguen usando —hay piezas de casas que se descontinuaron, ahí, ocupando espacio, porque quitarlas da miedo ("¿y si alguna casa todavía la usa?")—. La estantería crece con cada proyecto y se llena de piezas muertas.

El taller de ladrillos tiene una caja fija de ladrillos estándar: 2×4, plano, bisagra, teja. Esa caja es la misma el primer día y el último. Cada casa nueva se arma reusando los ladrillos que ya están en la caja; no se agregan tipos nuevos. Al año, la caja tiene exactamente los mismos tipos de ladrillo que al principio —montaste cien casas y la caja no creció—. Y no hay piezas muertas: un ladrillo o se usa en alguna casa o no está en la caja. Además, como todas las casas salen de los mismos ladrillos, todas encajan entre sí y se ven coherentes; no hay una casa con un verde "casi igual" al de las otras, porque no hay forma de mezclar un verde nuevo sin querer.

El CSS semántico es la estantería que crece y acumula piezas muertas. Las utilidades son la caja fija: los mismos tipos de ladrillo sirven para todas las pantallas, no crecen al agregar más, y la coherencia sale sola porque todo se arma de las mismas piezas. Ese "la caja no crece" es lo que vamos a medir.

Ejemplo trabajado: cuánto CSS agrega una pantalla nueva

El argumento más contraintuitivo de utility-first es que el CSS generado no crece al agregar componentes que reusan utilidades existentes. En el enfoque semántico, cada componente nuevo agrega su bloque de CSS. En utility-first, el CSS es el conjunto de utilidades distintas que usa toda la app —cada una emitida una vez—; un componente nuevo que solo reusa utilidades ya presentes agrega cero CSS.

Modelamos los dos enfoques en Node. Cada componente es una lista de utilidades. En utility-first, el CSS generado = el conjunto (la unión) de utilidades distintas. En semántico, cada componente aporta una clase con sus declaraciones, y esas declaraciones se repiten entre componentes. Medimos ambos con tres componentes, y luego agregamos un cuarto que solo reusa utilidades ya existentes:

// L3 — por que utilidades: el CSS generado NO crece al reusar. (modelo pedagogico)
const components = {
  ProductCard: ['flex', 'gap-2', 'p-4', 'rounded-md', 'bg-surface'],
  Button:      ['flex', 'p-4', 'rounded-md', 'bg-primary'],
  Badge:       ['p-4', 'rounded-md', 'bg-primary'],
};

// utilityCss: el conjunto de reglas que Tailwind emite = utilidades DISTINTAS (union).
function utilityCss(comps) {
  const set = new Set();
  for (const list of Object.values(comps)) for (const u of list) set.add(u);
  return set;
}

// semanticDeclarations: una clase por componente; cada una repite sus declaraciones.
function semanticDeclarations(comps) {
  let total = 0;
  for (const list of Object.values(comps)) total += list.length;
  return total;
}

const before = utilityCss(components);
console.log('=== 3 componentes ===');
console.log('  utilidades distintas (reglas de CSS generadas): ' + before.size);
console.log('  declaraciones si cada uno fuera una clase semantica: ' + semanticDeclarations(components));

// Agregamos un 4o componente que REUSA solo utilidades ya existentes.
const withChip = { ...components, Chip: ['p-4', 'rounded-md', 'bg-primary'] };
const after = utilityCss(withChip);
console.log('\n=== agregamos Chip (reusa p-4, rounded-md, bg-primary) ===');
console.log('  utilidades distintas ahora: ' + after.size + '  (delta CSS: ' + (after.size - before.size) + ')');
console.log('  declaraciones semanticas ahora: ' + semanticDeclarations(withChip) +
            '  (delta: ' + (semanticDeclarations(withChip) - semanticDeclarations(components)) + ')');

Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:

=== 3 componentes ===
  utilidades distintas (reglas de CSS generadas): 6
  declaraciones si cada uno fuera una clase semantica: 12

=== agregamos Chip (reusa p-4, rounded-md, bg-primary) ===
  utilidades distintas ahora: 6  (delta CSS: 0)
  declaraciones semanticas ahora: 15  (delta: 3)

Lee la salida en dos tiempos. Primero, con tres componentes: el enfoque de utilidades genera 6 reglas (las seis utilidades distintas que aparecen en toda la app: flex, gap-2, p-4, rounded-md, bg-surface, bg-primary), mientras que el semántico necesita 12 declaraciones (la suma de todas, con p-4 repetida en tres clases, rounded-md en tres, bg-primary en dos…). Ya con tres componentes, el enfoque de utilidades emite la mitad, porque deduplica: p-4 existe una vez y la referencian todos.

El segundo tiempo es el que importa. Agregamos un Chip que solo usa utilidades que ya existían (p-4, rounded-md, bg-primary). En el enfoque de utilidades, el CSS pasa de 6 reglas a… 6 reglas. Delta cero. No se generó ni una regla nueva, porque el Chip no trajo ninguna utilidad que no estuviera ya. En el semántico, agregar el Chip sumó 3 declaraciones más (su clase .chip con sus tres reglas), aunque sean idénticas a las que ya tenía el Badge. Un componente nuevo: cero CSS en un caso, más CSS —duplicado— en el otro.

Esa es la propiedad que hace que el CSS de una app grande con Tailwind se estabilice en un tamaño fijo mientras la app crece. El CSS deja de ser proporcional al número de pantallas y pasa a ser proporcional al número de utilidades distintas que usas —que tiene un techo, porque tu escala tiene un número finito de pasos—. La estantería del taller semántico crece con cada casa; la caja de ladrillos no. (Y lo que Tailwind hace además —purgar del CSS las utilidades que ni siquiera usas— achica aún más ese conjunto; eso es tema de la guía de performance.)

Los otros tres argumentos no necesitan medición, pero conviene nombrarlos con precisión. Velocidad: estilar sin cambiar de archivo ni inventar nombres es, en la práctica, mucho más rápido —escribes p-4 en el marcado y sigues, sin abrir la hoja de estilos ni decidir cómo llamar a la clase—. No nombrar todo: "nombrar cosas" es célebre por ser de lo más difícil de programar; utility-first lo elimina para el 95% de los elementos, que ya no necesitan un nombre. Consistencia por la escala: como p-4 sale de una escala y no de un padding: 15px inventado, es imposible meter sin querer un espaciado "casi igual" —no hay p-3.5 a mano; usas los pasos que existen—. Ese último es el puente al módulo 4.

Una pregunta hacia la lección 5: si p-4 es la misma clase en el ProductCard, el Button, el Badge y el Chip, y todas comparten esa única regla .p-4 { padding: 1rem; }… ¿qué pasa cuando dos utilidades que tocan la misma propiedad caen en el mismo elemento? ¿Cuál gana? (Guarda la pregunta; la especificidad igual de todas las utilidades la responde en la lección 5.)

Profundización: el mito de la "separación de intereses"

La objeción clásica a utility-first —la que Adam Wathan desarma en su ensayo— es que "mezclar estilo y estructura viola la separación de intereses". La idea, heredada de los 2000, era que el HTML lleva la estructura y el CSS el estilo, y que meterlos juntos es sucio. Suena bien, pero esconde un supuesto falso: que el HTML y el CSS son independientes. No lo son. Cuando cambias el diseño de una tarjeta, tocas su marcado y su CSS; cuando agregas un elemento, agregas su estructura y su estilo. Están acoplados por naturaleza —dependen el uno del otro—, y fingir que están separados solo esconde ese acoplamiento en dos archivos que siempre cambias juntos.

Wathan propone verlo al revés: la separación real no es "HTML vs CSS", es "HTML que depende del CSS vs CSS que depende del HTML". En el enfoque semántico, tu CSS depende de tu HTML: la clase .product-card existe porque hay un componente product-card; si renombras el componente, renombras la clase. Es un acoplamiento donde el CSS sigue al HTML. En utility-first se invierte: tu HTML depende de un CSS de utilidades fijo y agnósticop-4 no sabe ni le importa que exista un product-card—. El CSS deja de perseguir a tus componentes; se vuelve una capa estable que tus componentes consumen. Esa inversión es exactamente lo que hace que la caja de ladrillos no crezca: los ladrillos no dependen de las casas.

Errores comunes

Descartar utility-first por "viola la separación de intereses". Qué pasa: se rechaza el modelo con el argumento de que mezclar estilo y estructura es sucio. Por qué pasa: es una regla que se enseñó como dogma sin examinar su supuesto. Cómo detectarlo: tu objeción es de principio ("no se debe"), no de una consecuencia concreta que hayas medido. Cómo corregirlo: el HTML y el CSS ya están acoplados —cambian juntos—; el enfoque semántico solo esconde ese acoplamiento en dos archivos. Utility-first lo hace explícito y, a cambio, te da un CSS estable que no persigue a tus componentes. No es que la separación de intereses sea mala; es que "HTML vs CSS" nunca fue la línea correcta de separación.

Creer que más clases en el class significa más CSS descargado. Qué pasa: se ve class="flex gap-2 p-4 rounded-md bg-surface" y se piensa "cinco clases, esto pesa más que una .card". Por qué pasa: se confunde el tamaño del atributo class en el HTML con el tamaño del CSS. Cómo detectarlo: tu argumento contra utility-first es sobre el peso del marcado. Cómo corregirlo: las cinco clases del class referencian cinco reglas que se descargan una vez y se comparten con toda la app —el HTML repite el nombre de la clase, no su contenido—. Como viste, el CSS generado no crece al reusar; lo que crece un poco es el marcado (repites nombres de clase), y eso comprime muy bien y no es el cuello de botella. El peso real —el CSS— es el que se mantiene fijo.

Extraer a clases semánticas "para que no se repita" y perder la ventaja. Qué pasa: se ve flex p-4 rounded-md bg-primary repetido en dos componentes y se extrae a una clase .card con @apply de inmediato. Por qué pasa: el instinto DRY ("no te repitas") dispara al ver la repetición. Cómo detectarlo: tu CSS vuelve a llenarse de clases con nombre de componente en cuanto una combinación aparece dos veces. Cómo corregirlo: repetir utilidades en el marcado no repite CSS —la regla .p-4 sigue siendo una sola—, así que la repetición que ves es de nombres, no de estilos, y no cuesta lo que el instinto DRY cree. Extraer con @apply tiene su lugar (lección 7), pero hacerlo apenas algo se repite dos veces te devuelve la estantería que crece. La regla: extrae cuando la combinación se repita idéntica muchas veces y duela mantenerla, no a la primera repetición.

Ejercicios

Ejercicio 1 — Cuenta el CSS. Tienes dos componentes: Alert usa flex p-4 rounded-md bg-primary y Toast usa flex p-4 rounded-md bg-surface. (a) ¿Cuántas utilidades distintas generan entre los dos (el CSS de utilidades)? (b) ¿Cuántas declaraciones sumaría el enfoque semántico (una clase por componente)? (c) Si agregas un Snackbar con flex p-4 rounded-md bg-primary (idéntico a Alert), ¿cuánto CSS nuevo agrega en cada enfoque?

Ver solución
  • (a) 5 utilidades distintas. La unión de ambos: flex, p-4, rounded-md, bg-primary, bg-surface. flex, p-4 y rounded-md se comparten; solo el bg-* difiere. Cinco reglas.
  • (b) 8 declaraciones. Cada componente tiene 4 declaraciones y el semántico no deduplica: 4 (Alert) + 4 (Toast) = 8, con flex, p-4 y rounded-md repetidas en ambas clases.
  • (c) Utilidades: cero. El Snackbar solo usa utilidades ya presentes (flex p-4 rounded-md bg-primary), así que la unión no cambia: sigue en 5. Semántico: +4. Suma una clase .snackbar con sus 4 declaraciones, idénticas a las de .alert. Un componente nuevo idéntico: cero CSS vs cuatro declaraciones duplicadas.

Ejercicio 2 — Empareja el argumento. Para cada situación, di cuál de los cuatro argumentos de utility-first la resuelve (velocidad, no nombrar todo, CSS que no crece, consistencia por la escala):

  • (a) Un dev pasa diez minutos decidiendo si la clase se llama .card, .product-tile o .item-box.
  • (b) La hoja de estilos tiene 4.000 líneas y nadie sabe cuáles clases se siguen usando.
  • (c) Una tarjeta tiene padding: 15px y las demás 16px; nadie sabe por qué.
  • (d) Estilar un botón nuevo sin abrir la hoja de estilos ni salir del componente.
Ver solución
  • (a) No nombrar todo. El problema es inventar un nombre; utility-first lo elimina —el botón no necesita clase con nombre—.
  • (b) CSS que no crece. La hoja gigante con clases muertas es la estantería del taller semántico; con utilidades el CSS es un conjunto fijo y sin piezas muertas (una utilidad se usa o no está).
  • (c) Consistencia por la escala. El 15px inventado no puede pasar si usas p-4 (que es 1rem fijo); no hay forma de meter un valor "casi igual" fuera de la escala. (Apunta al módulo 4.)
  • (d) Velocidad. Estilar sin cambiar de archivo ni nombrar nada es el argumento de velocidad en acción.

Ejercicio 3 — Predice la salida. Sin correr nada, di qué imprimiría la primera parte del ejemplo (=== 3 componentes ===) si el Badge usara ['flex', 'p-8', 'rounded-md', 'bg-primary'] (con p-8 en vez de p-4).

Ver solución

Cambiar p-4 por p-8 en el Badge agrega una utilidad distinta nueva (p-8 no estaba en ningún otro componente), así que la unión sube de 6 a 7. Las declaraciones semánticas no cambian en número (el Badge sigue teniendo 4):

=== 3 componentes ===
  utilidades distintas (reglas de CSS generadas): 7
  declaraciones si cada uno fuera una clase semantica: 12

La lección: el CSS de utilidades crece solo cuando usas una utilidad nueva (un paso de la escala que no habías tocado), no cuando reusas. p-8 es un ladrillo que antes no estaba en la caja; por eso suma uno. Si el Badge hubiera seguido con p-4, la caja no habría crecido.

Resumen y siguiente paso

En esta lección justificaste utility-first con cuatro argumentos: velocidad (estilar sin cambiar de archivo ni nombrar), no nombrar todo (elimina el problema más difícil para la mayoría de los elementos), un CSS que no crece al reusar, y consistencia por la escala (las utilidades leen de pasos fijos, no de valores inventados). Con los dos talleres viste la imagen central: el CSS semántico es una estantería que crece y acumula piezas muertas; las utilidades son una caja de ladrillos fija que no crece al montar más casas. Y lo mediste: agregar un componente que reusa utilidades existentes sumó cero CSS en el enfoque de utilidades y declaraciones duplicadas en el semántico. También desarmaste la objeción de la "separación de intereses": HTML y CSS ya están acoplados, y utility-first invierte la dependencia para darte un CSS estable que tus componentes consumen.

Antes de avanzar deberías poder: nombrar los cuatro argumentos y dar un ejemplo de cada uno; explicar por qué el CSS de utilidades no crece al reusar; y responder la objeción de la separación de intereses.

La lección 4 abre la caja negra: cómo funciona Tailwind por debajo. Hasta aquí tratamos las utilidades como ladrillos que "simplemente funcionan". Ahora vas a ver el mecanismo exacto —cada utilidad es una clase de una declaración, generada por adelantado— con el tw() que mapea una lista de clases al bloque de CSS que el elemento recibe, corrido sobre el botón del product-card de Mercado. Es el momento de ver, ejecutado, qué hay dentro del ladrillo.

Recursos