Módulo 3: Utility First With Tailwind

Cómo funciona Tailwind por debajo

Descripción

Hasta aquí tratamos las utilidades como ladrillos que "simplemente funcionan": escribes p-4 y aparece el padding. Esta lección abre la caja y muestra el mecanismo, que es sorprendentemente simple: Tailwind no hace nada mágico en tiempo de ejecución. Genera, por adelantado, una hoja de estilos con miles de clases diminutas —cada una con una sola declaración—, y cuando pones class="p-4" en un elemento, el navegador aplica esa clase como aplicaría cualquier otra clase de CSS. No hay un motor de Tailwind corriendo en el navegador; hay una hoja de CSS normal, muy grande, generada de tu escala y tus tokens. Entender esto disuelve casi todo el "misterio" de Tailwind: es CSS común, escrito por una herramienta en vez de por ti.

Para ver el mecanismo, esta lección construye el modelo central del módulo: tw(classNames), una función que toma la lista de clases de un elemento y devuelve el bloque de declaraciones CSS que ese elemento recibe —exactamente lo que Tailwind resuelve por debajo, para un subconjunto pedagógico de utilidades—. Lo corremos sobre el botón "Add to cart" del product-card de Mercado, y verás salir el bloque de CSS completo que el botón obtiene al coleccionar sus seis utilidades.

Conexión con el módulo. La lección 2 mostró qué es estilar con utilidades y la 3 por qué; esta muestra cómo, por dentro. Es la lección más mecánica del módulo, y el tw() que construyes aquí es la herramienta que reaparece en la 5 (para ver que dos utilidades sobre la misma propiedad chocan), en la 6 (para ver que bg-primary genera una declaración que apunta a tu token) y en el proyecto (para estilar el product-card entero). Aquí instalamos el motor; el resto del módulo lo usa.

Una analogía: el catálogo de ladrillos ya fabricados

Imagina la fábrica de ladrillos LEGO. No fabrica un ladrillo cuando tú lo pides; fabricó todos los tipos de ladrillo por adelantado y los tiene en el catálogo: el 2×4 rojo, el 2×4 azul, el plano, la bisagra, la teja… miles de referencias, cada una ya moldeada, esperando en su casillero. Cuando armas una casa, no encargas fabricación: tomas del catálogo los ladrillos que ya existen y los encajas.

Tailwind es esa fábrica. Antes de que tú escribas una línea de marcado, ya generó el catálogo completo de utilidades —.p-0, .p-1, … .p-96, .flex, .grid, .bg-primary, .text-lg, miles de clases—, cada una con su única declaración, esperando en la hoja de estilos. Cuando escribes class="p-4 flex bg-primary", no "generas CSS al vuelo": el navegador va al catálogo (la hoja) y aplica las clases que ya estaban ahí. Por eso Tailwind no necesita correr en el navegador —su trabajo (generar el catálogo) ya terminó antes—, y por eso p-4 es instantáneo: es una clase pre-fabricada, no un cálculo.

Hay un detalle de la fábrica que importa para la próxima lección: los ladrillos del catálogo están en un orden fijo —primero todos los rojos, luego los azules, o como sea que la fábrica los ordene—. Ese orden no lo decides tú al armar la casa; lo decidió la fábrica al imprimir el catálogo. Guarda ese detalle: en la lección 5 verás que ese orden fijo del catálogo (el orden de las utilidades en la hoja generada) es lo que decide quién gana cuando dos utilidades chocan —no el orden en que las escribes en el class—.

Ejemplo trabajado: tw() sobre el botón del product-card

Construyamos el motor. tw(classNames) toma una cadena de clases y devuelve el bloque de declaraciones que el elemento recibe. Por dentro, para cada clase llama a util(cls) —el mapeo de una utilidad a su(s) declaración(es)— y junta todo. Es, en pequeño, lo que Tailwind hace: resolver cada utilidad a su regla y componer el estilo del elemento.

Lo corremos sobre el botón "Add to cart" del product-card, que usa las seis utilidades del subconjunto: flex gap-2 p-4 rounded-md bg-primary text-lg.

// L4 — como funciona por debajo: tw(classNames) mapea utilidades -> declaraciones CSS.
// SUBCONJUNTO PEDAGOGICO (p-4, gap-2, bg-*, text-lg, flex, rounded-md), no Tailwind real.
const spacing  = { '2': '0.5rem', '4': '1rem', '8': '2rem' };       // base 4px: step * 0.25rem
const fontSize = { 'text-sm': ['0.875rem', '1.25rem'],
                   'text-base': ['1rem', '1.5rem'],
                   'text-lg': ['1.125rem', '1.75rem'] };

// util(cls): UNA utilidad -> su(s) declaracion(es).
function util(cls) {
  if (cls === 'flex')       return ['display: flex'];
  if (cls === 'rounded-md') return ['border-radius: 0.375rem'];
  if (cls in fontSize) { const [fs, lh] = fontSize[cls]; return ['font-size: ' + fs, 'line-height: ' + lh]; }
  let m;
  if ((m = cls.match(/^p-(\d+)$/)))     return ['padding: ' + spacing[m[1]]];
  if ((m = cls.match(/^gap-(\d+)$/)))    return ['gap: ' + spacing[m[1]]];
  if ((m = cls.match(/^bg-([a-z]+)$/)))  return ['background-color: var(--color-' + m[1] + ')'];
  if ((m = cls.match(/^text-([a-z]+)$/))) return ['color: var(--color-' + m[1] + ')'];
  throw new Error('utilidad fuera del subconjunto: ' + cls);
}

// tw(classNames): la lista de clases -> el bloque de declaraciones combinado.
function tw(classNames) {
  const decls = [];
  for (const cls of classNames.trim().split(/\s+/)) for (const d of util(cls)) decls.push(d);
  return decls;
}

// El "Add to cart" del product-card de Mercado, estilado con utilidades.
const buttonClasses = 'flex gap-2 p-4 rounded-md bg-primary text-lg';

console.log('class="' + buttonClasses + '"\n');
console.log('=== tw() expande cada utilidad a su declaracion ===');
for (const cls of buttonClasses.split(/\s+/)) {
  console.log('  .' + cls.padEnd(11) + '-> ' + util(cls).join('; '));
}
console.log('\n=== el bloque de CSS que el elemento recibe ===');
console.log('{');
for (const d of tw(buttonClasses)) console.log('  ' + d + ';');
console.log('}');

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

class="flex gap-2 p-4 rounded-md bg-primary text-lg"

=== tw() expande cada utilidad a su declaracion ===
  .flex       -> display: flex
  .gap-2      -> gap: 0.5rem
  .p-4        -> padding: 1rem
  .rounded-md -> border-radius: 0.375rem
  .bg-primary -> background-color: var(--color-primary)
  .text-lg    -> font-size: 1.125rem; line-height: 1.75rem

=== el bloque de CSS que el elemento recibe ===
{
  display: flex;
  gap: 0.5rem;
  padding: 1rem;
  border-radius: 0.375rem;
  background-color: var(--color-primary);
  font-size: 1.125rem;
  line-height: 1.75rem;
}

Lee la primera mitad como el catálogo consultado, ladrillo por ladrillo. Cada una de las seis clases del botón resuelve a su declaración: flex a display: flex, gap-2 a gap: 0.5rem, y así. Fíjate en text-lg: devuelve dos declaraciones (font-size y line-height). No es una excepción a "una utilidad, una cosa" —es que "tamaño de texto grande" es un par indivisible: un tamaño de fuente con su interlineado correspondiente, que en tipografía siempre van juntos—. Casi todas las utilidades son una declaración; unas pocas, como las de tamaño de texto, son un par que tiene sentido como unidad.

Lee la segunda mitad como el estilo final del botón. tw() juntó las siete declaraciones (seis utilidades, pero text-lg aportó dos) en un solo bloque —exactamente el CSS que el navegador aplica al botón—. Nadie escribió una clase .add-to-cart-button; el botón obtuvo su estilo completo coleccionando seis clases del catálogo. Ese bloque es lo que una clase semántica habría contenido, reconstruido a partir de utilidades.

Y fíjate, de nuevo, en bg-primary: la declaración es background-color: var(--color-primary), no un hex. La utilidad de color apunta a tu token del módulo 2. Cuando el navegador pinte el botón, resolverá var(--color-primary) al valor que el token tenga en el tema activo —#2563eb en claro, #60a5fa en oscuro—. La utilidad genera la referencia; el token provee el valor; y el enganche entre ambos es la lección 6.

Una observación mecánica que prepara la lección 5: tw() juntó las declaraciones en el orden en que las clases aparecen en el class. Con estas seis utilidades no hay problema, porque cada una toca una propiedad distinta —no hay conflicto—. Pero, ¿qué pasaría si dos utilidades tocaran la misma propiedad? ¿Decide el orden en que las escribí en el class, como sugiere este modelo, o algo más? (La respuesta te sorprenderá: no decide el orden del class, sino el orden del catálogo. La lección 5 lo demuestra, y es la razón de que este modelo de tw() sea una simplificación.)

Profundización: por qué Tailwind es "solo CSS" (y qué implica)

Que Tailwind genere CSS estático por adelantado —en vez de correr en el navegador— tiene tres consecuencias que vale la pena hacer explícitas.

No hay costo en tiempo de ejecución. A diferencia de las soluciones "CSS-in-JS" que calculan estilos mientras la página corre, Tailwind terminó su trabajo antes de que el usuario cargue la página. El navegador solo recibe una hoja de CSS normal. Es rápido porque no hay nada que calcular al vuelo —los ladrillos ya estaban fabricados—.

El CSS es predecible y depurable. Como cada utilidad es una clase de CSS real en una hoja real, puedes abrir las herramientas del navegador, inspeccionar el botón, y ver .bg-primary { background-color: var(--color-primary); } como verías cualquier regla. No hay una capa opaca entre tu marcado y el CSS; p-4 es .p-4 { padding: 1rem }, y está ahí para que la inspecciones. Esto conecta directo con web-fundamentals: todo lo que aprendiste a depurar en la cascada aplica igual, porque las utilidades son CSS ordinario.

El tamaño se controla purgando. Como el catálogo completo es enorme (miles de clases), Tailwind, al construir para producción, elimina del CSS final toda utilidad que no aparezca en tu marcado —queda solo lo que de verdad usas—. Por eso el CSS de una app real con Tailwind suele pesar poco a pesar del catálogo gigante. El detalle de cómo purga es de la guía de performance; aquí basta saber que el catálogo es grande en teoría pero pequeño en tu bundle final.

Errores comunes

Creer que Tailwind "corre" en el navegador o interpreta tus clases al vuelo. Qué pasa: se imagina un motor de Tailwind que lee tu class y calcula estilos en tiempo real. Por qué pasa: el resultado se siente dinámico ("escribo p-4 y aparece"). Cómo detectarlo: te preocupa el "costo de ejecución de Tailwind" o buscas su script en el navegador. Cómo corregirlo: Tailwind genera CSS estático antes de servir la página; en el navegador solo hay una hoja de CSS normal. p-4 es una clase pre-fabricada, no un cálculo. No hay motor corriendo; el trabajo ya terminó en el build.

Pensar que text-lg viola "una utilidad, una declaración" porque trae dos. Qué pasa: se ve que text-lg aporta font-size y line-height y se concluye que el modelo "una declaración por utilidad" es falso. Por qué pasa: la mayoría de las utilidades sí son una sola declaración, así que el par sorprende. Cómo detectarlo: cuentas dos declaraciones donde esperabas una. Cómo corregirlo: unas pocas utilidades representan un concepto que es indivisible en dos propiedades —"texto grande" es tamaño más interlineado, que en tipografía van juntos—. No rompen el modelo: siguen haciendo una cosa (fijar el tamaño de texto), solo que esa cosa se expresa en dos propiedades CSS. La regla mental correcta es "una utilidad, un propósito", y casi siempre ese propósito es una declaración.

Inspeccionar el marcado buscando dónde se definió el estilo, en vez de inspeccionar el CSS. Qué pasa: al depurar, se busca en el HTML por qué el botón tiene ese padding, sin mirar la hoja de estilos. Por qué pasa: como el estilo está "en el marcado" (el class), uno cree que la definición también. Cómo detectarlo: no encuentras de dónde sale un valor y culpas al marcado. Cómo corregirlo: el class solo referencia clases; la definición vive en la hoja generada. Abre las herramientas del navegador e inspecciona el elemento: verás .p-4 { padding: 1rem } como una regla real. Depurar Tailwind es depurar CSS normal —lo que aprendiste en web-fundamentals aplica intacto—.

Ejercicios

Ejercicio 1 — Expande a mano. Sin correr nada, escribe el bloque de declaraciones que tw('flex p-8 rounded-md bg-surface') produciría, usando el subconjunto del ejemplo.

Ver solución

Cada utilidad resuelve a su declaración (p-8 = 8 × 0.25rem = 2rem; bg-surface apunta a su token):

{
  display: flex;
  padding: 2rem;
  border-radius: 0.375rem;
  background-color: var(--color-surface);
}

Cuatro utilidades, cuatro declaraciones (ninguna es un par como text-lg). El bloque es el estilo completo que un elemento con ese class recibiría —lo que una clase .panel { ... } habría contenido—.

Ejercicio 2 — Detecta la utilidad de dos declaraciones. De estas utilidades, ¿cuál produce dos declaraciones y por qué? p-4, text-base, flex, rounded-md.

Ver solución

text-base produce dos: font-size: 1rem y line-height: 1.5rem. La razón es la misma que con text-lg: una utilidad de tamaño de texto fija el tamaño de fuente y su interlineado, porque en tipografía el interlineado se define en relación al tamaño y los dos van juntos. Las otras tres son una sola declaración cada una (padding, display, border-radius). El propósito de text-base sigue siendo uno —"texto de tamaño base"—; solo que se expresa en dos propiedades.

Ejercicio 3 — Predice la salida. Sin correr nada, di qué imprimiría la sección "el bloque de CSS que el elemento recibe" si cambiáramos buttonClasses a 'flex gap-2 p-4 bg-surface' (sin rounded-md ni text-lg).

Ver solución

Cuatro utilidades, cada una una declaración, en el orden del class:

{
  display: flex;
  gap: 0.5rem;
  padding: 1rem;
  background-color: var(--color-surface);
}

Al quitar text-lg desaparece el par font-size/line-height (por eso ahora son cuatro declaraciones, no siete), y al quitar rounded-md desaparece el border-radius. bg-surface cambió el token referenciado de --color-primary a --color-surface. El bloque refleja exactamente las utilidades presentes, en su orden.

Resumen y siguiente paso

En esta lección abriste la caja negra: Tailwind genera por adelantado una hoja de CSS con miles de clases de una declaración, y class="p-4" aplica una de esas clases pre-fabricadas —no hay motor corriendo en el navegador, es CSS normal—. Con el catálogo de ladrillos ya fabricados viste el mecanismo: la fábrica moldeó todos los tipos por adelantado y tú tomas del catálogo. Y construiste el motor del módulo, tw(), corriéndolo sobre el botón del product-card: sus seis utilidades resolvieron a siete declaraciones (con text-lg aportando el par font-size/line-height), y el bloque final —con bg-primary apuntando a tu token— es exactamente el estilo del botón. También viste las tres consecuencias de que Tailwind sea "solo CSS": cero costo en ejecución, CSS predecible y depurable, y tamaño controlado por purga.

Antes de avanzar deberías poder: explicar que Tailwind genera CSS estático por adelantado y no corre en el navegador; describir qué hace tw()/util() para reconstruir el estilo de un elemento; y decir por qué una utilidad como text-lg produce dos declaraciones sin romper el modelo.

La lección 5 recoge la pregunta que tw() dejó abierta: cuando dos utilidades tocan la misma propiedad, ¿cuál gana? Y la respuesta conecta este módulo con web-fundamentals: como todas las utilidades son clases de la misma especificidad [0,1,0], ninguna le gana a otra por especificidad —deciden por orden de fuente, el orden en el CSS generado, no el orden en tu class—. Vas a ejecutar la demostración con p-4 y p-8 sobre el mismo elemento y ver, medido, por qué el orden del catálogo manda.

Recursos