Módulo 3: Utility First With Tailwind

Presentación del módulo: utility-first con Tailwind

De los tokens a las utilidades que los usan

En el módulo 2 fabricaste los cimientos: el set de tokens de Mercado en tres capas —primitivos (blue.600), semánticos (color.primary), de componente (button.bg)— resuelto con resolveToken y escrito como CSS custom properties en un bloque :root (tema claro) y .dark (tema oscuro). Al cerrar el módulo tenías --color-primary: #2563eb viviendo en tu hoja de estilos, listo para que algo lo consumiera. Pero no escribiste una sola pieza que lo usara. Definiste la paleta; no pintaste con ella.

Aquí empiezas a pintar. Este módulo sube un escalón en las cuatro capas del sistema —de tokens a utilidades— y presenta la herramienta que las hace prácticas: Tailwind CSS. Una utilidad es una clase de CSS diminuta que hace una sola cosa: p-4 pone padding: 1rem, flex pone display: flex, bg-primary pone el fondo con tu token de color. En vez de escribir una hoja de estilos con clases semánticas (.product-card { ... veinte líneas ... }), construyes la UI combinando utilidades directo en el marcado: <div class="flex gap-2 p-4 rounded-md bg-surface">. Al terminar sabrás qué es el modelo utility-first, por qué se prefiere a escribir CSS semántico a mano, cómo funciona por debajo (spoiler: es la cascada que aprendiste en web-fundamentals), cómo enganchar Tailwind a tus tokens del módulo 2, y cuándo agrupar utilidades con @apply.

No entramos todavía en todo lo que Tailwind hace encima de esto. Los prefijos responsive (md:, lg:) y la variante dark: son el módulo 6. Las variantes de componente (variant, size, el patrón cva) son el módulo 5. Las escalas concretas —cuántos pasos de espaciado, qué ratios tipográficos— son el módulo 4. Aquí nos quedamos en el modelo base: qué es una utilidad, por qué gana, y cómo se conecta con tus tokens. Es la capa que traduce "tengo tokens" en "puedo construir pantallas rápido y consistentes".

Conexión con el módulo. Esta es la lección-mapa del módulo 3. No entra a fondo en ninguna pieza: instala la tesis (una utilidad es una clase de una sola declaración que consume tus tokens, y su poder viene de la cascada), da el mapa de las ocho lecciones, y ejecuta un primer teaser que traduce una utilidad a su CSS. La lección 2 define qué es utility-first. La 3 argumenta por qué utilidades y no clases semánticas. La 4 abre la caja: cómo funciona Tailwind por debajo, con el tw() que mapea utilidades a declaraciones. La 5 conecta con la cascada de web-fundamentals: por qué el orden de fuente decide, no el orden del class="". La 6 engancha Tailwind a tus tokens (theme.extend). La 7 cubre @apply y cuándo extraer. Y la 8 te pone a estilar el product-card de Mercado con utilidades conectadas a los tokens del módulo 2.

Una analogía: las piezas LEGO de una sola función

Imagina dos formas de armar la pared de una casa de juguete.

La primera: cada vez que necesitas una pieza, mandas a fabricarla a medida. "Necesito el panel frontal de la casa —ventana a la izquierda, puerta al centro, ese verde exacto—", y encargas una pieza única, moldeada para ese uso, con su nombre en una etiqueta: panel-frontal. Sirve perfecto… para esa casa. Para la casa de al lado, que es casi igual pero con la ventana a la derecha, encargas otra pieza custom: panel-frontal-vecino. Tu caja se llena de piezas únicas, cada una con su nombre, cada una usada una vez. Y cuando quieres cambiar el verde de todas, tienes que reencargar cada panel. Ese es el CSS semántico a mano: una clase con nombre por cada componente, moldeada a medida, difícil de reusar.

La segunda: tienes una caja de ladrillos LEGO estándar, cada uno con una sola función —un ladrillo de 2×4, uno plano, una bisagra, una teja—. No armas "el panel frontal" de un tirón; lo compones encajando ladrillos: dos filas de 2×4, un hueco para la ventana, una bisagra para la puerta. Para la casa vecina reusas los mismos ladrillos en otro orden. Ningún ladrillo tiene nombre de "para qué sirve en esta casa"; el ladrillo de 2×4 es siempre el ladrillo de 2×4, lo uses donde lo uses. Esas son las utilidades: p-4 siempre pone el mismo padding, flex siempre pone la misma disposición, los combines donde los combines. Construyes la pieza componiendo, no encargando.

¿Y cuándo conviene una pieza con nombre? Cuando una combinación de ladrillos se repite idéntica en veinte lugares —digamos, "el botón de comprar", siempre los mismos ocho ladrillos en el mismo orden—. Ahí pegas esos ladrillos entre sí y les das un nombre: btn. Eso es @apply: agrupar utilidades que siempre van juntas en una clase reutilizable. Pero es la excepción, no la regla —si le pones nombre a cada combinación, vuelves a la caja llena de piezas únicas, y perdiste la ventaja de los ladrillos estándar—. La lección 7 fija cuándo.

Guarda la imagen: las utilidades son ladrillos de una función que combinas; el CSS semántico es mandar a fabricar una pieza custom con nombre cada vez; @apply es pegar los ladrillos que siempre van juntos. Todo el módulo desarrolla esa distinción.

Ejemplo trabajado: una utilidad es una clase de una declaración

El módulo 2 te dejó con tokens escritos en CSS. Este teaser muestra el primer eslabón del módulo 3: qué es una utilidad, por dentro. La respuesta es casi decepcionante de tan simple: una utilidad es una clase de CSS con una sola declaración. p-4 no es magia; es literalmente .p-4 { padding: 1rem; }. Tailwind genera miles de esas clases diminutas por adelantado, y tú las combinas en el marcado.

Como Tailwind no corre en un agente, modelamos su traducción con puro JavaScript. Una función util(cls) toma el nombre de una utilidad y devuelve la declaración CSS que representa —el mismo mapeo que Tailwind hace internamente, para un subconjunto pedagógico de utilidades—:

// L1 intro — una utilidad = UNA clase con UNA declaracion. Subconjunto pedagogico (no Tailwind real).
const spacing = { '2': '0.5rem', '4': '1rem', '8': '2rem' }; // base 4px: step * 0.25rem

// util(cls): dada UNA utilidad, devuelve su(s) declaracion(es) CSS.
function util(cls) {
  if (cls === 'flex')       return ['display: flex'];
  if (cls === 'rounded-md') return ['border-radius: 0.375rem'];
  let m;
  if ((m = cls.match(/^p-(\d+)$/)))  return ['padding: ' + spacing[m[1]]];
  if ((m = cls.match(/^gap-(\d+)$/))) return ['gap: ' + spacing[m[1]]];
  throw new Error('utilidad fuera del subconjunto: ' + cls);
}

console.log('=== una utilidad = una clase de una declaracion ===\n');
for (const cls of ['flex', 'p-4', 'gap-2', 'rounded-md']) {
  const decls = util(cls).join('; ');
  console.log('  .' + cls.padEnd(11) + '{ ' + decls + '; }');
}

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

=== una utilidad = una clase de una declaracion ===

  .flex       { display: flex; }
  .p-4        { padding: 1rem; }
  .gap-2      { gap: 0.5rem; }
  .rounded-md { border-radius: 0.375rem; }

Lee cada línea como lo que Tailwind ya tiene generado en tu hoja de estilos. .flex es una clase que hace una cosa: display: flex. .p-4 hace una cosa: padding: 1rem. No hay lógica, no hay nombre de componente, no hay "para qué sirve": es un ladrillo de una función. Cuando escribes <div class="flex p-4">, el navegador aplica las dos clases y el div recibe display: flex; padding: 1rem; —no porque Tailwind "entienda" tu div, sino porque esas dos clases diminutas ya existían y tu elemento las coleccionó—.

Fíjate en de dónde sale 1rem para p-4. No es arbitrario: la 4 es un paso en una escala de espaciado con base 0.25rem (4px), así que p-4 = 4 × 0.25rem = 1rem. Esa escala es justo lo que da coherencia —todos tus paddings salen de la misma regla numérica, no de valores inventados— y es el tema del módulo 4. Aquí solo nota que la utilidad lee de una escala; no la construimos todavía.

Y fíjate en lo que no hay: ninguna de estas clases dice bg-primary con un color adentro. Falta el eslabón que conecta con el módulo 2. En el CSS real, bg-primary no llevará #2563eb escrito; llevará background-color: var(--color-primary) —una referencia a tu token—. Esa es la unión de las dos capas, y es la lección 6. Por ahora, guarda la idea: la utilidad es el ladrillo; el token es de qué está hecho el ladrillo.

Una pregunta para que la cargues por el resto del módulo: si <div class="flex p-4"> y <span class="p-4"> usan la misma clase .p-4, ¿cuántas veces aparece .p-4 { padding: 1rem; } en el CSS que el navegador descarga? (Una sola vez —la clase se define una vez y la referencian mil elementos—. Guarda esa intuición: es la razón, en la lección 3, de que el CSS de una app con Tailwind no crezca al agregar pantallas.)

El mapa del módulo

Guarda esta ruta; es cómo cada lección construye la capa de utilidades sobre los tokens:

Idea                                       Lección   Concepto clave
─────────────────────────────────────────  ────────  ─────────────────────────────────────
Qué es utility-first                       L2        estilar componiendo utilidades atomicas
                                                     en el marcado, no clases semanticas
Por qué utilidades y no semanticas         L3        velocidad, no nombrar todo, CSS que no
                                                     crece, consistencia por la escala
Como funciona por debajo                   L4        tw(): cada utilidad = una clase de una
                                                     declaracion; el bloque final del elemento
Utilidades y la cascada                     L5        igual especificidad [0,1,0] -> gana el
                                                     orden de FUENTE, no el del class=""
Configurar Tailwind con tus tokens         L6        theme.extend: bg-primary -> var(--color-
                                                     primary); la utilidad hereda tu theming
@apply y cuando extraer                    L7        agrupar utilidades que SIEMPRE van juntas;
                                                     y por que abusar reconstruye CSS semantico
─────────────────────────────────────────  ────────  ─────────────────────────────────────
Proyecto: el product-card con utilidades   L8        estilar el product-card de Mercado con
                                                     utilidades + tokens; orden de fuente

La frontera: qué NO entra en este módulo

Saber la frontera te evita mezclar lo que otras lecciones y guías cubren:

  • Los prefijos responsive (md:, lg:) y la variante dark: son el módulo 6. Aquí una utilidad aplica siempre; cómo hacer que aplique solo en cierto ancho o tema es allá. (Y el CSS responsive "a mano" —las media queries— fue web-fundamentals módulo 7.)
  • Las variantes de componente (variant="primary", size="lg", el patrón cva) son el módulo 5. Aquí combinas utilidades a mano en el marcado; convertir esas combinaciones en un componente configurable por props es allá.
  • Las escalas concretas —los pasos de espaciado, los ratios tipográficos, los steps 50–900 de color, el contraste WCAG— son el módulo 4. Aquí las utilidades leen de una escala (p-4 = 1rem), pero no construimos ni justificamos la escala.
  • La cascada y la especificidad a fondo (el box model, resolve, !important) son web-fundamentals módulo 3, un prerequisito. Aquí lo asumimos y lo conectamos: verás que las utilidades ganan por orden de fuente porque todas tienen la misma especificidad [0,1,0] —pero no re-enseñamos qué es la especificidad—.
  • El bundle de CSS y cómo Tailwind purga lo no usado (que el CSS final solo incluya las utilidades que de verdad aparecen en tu marcado) es performance y deploy, otra guía. Aquí lo mencionamos como una consecuencia del modelo, sin entrar.

Errores comunes

Creer que utility-first es "escribir CSS en línea con otro nombre". Qué pasa: alguien ve class="flex p-4 bg-primary" y piensa "esto es style="" disfrazado, un desorden". Por qué pasa: visualmente se parece —los estilos están en el marcado, no en una hoja aparte—. Cómo detectarlo: rechazas Tailwind por "ensucia el HTML" sin haber visto qué genera. Cómo corregirlo: style="padding: 16px" es un valor crudo, único, sin escala, sin theming, con especificidad de estilo en línea que pelea con todo. class="p-4" es una referencia a una clase que sale de una escala, que puede apuntar a un token, y que tiene especificidad de clase normal —participa en la cascada como cualquier otra—. Son opuestos: uno es un valor suelto pegado al elemento; el otro es un sistema de clases reutilizables. La lección 2 marca la diferencia; la 5, por qué la especificidad lo cambia todo.

Querer nombrar cada combinación de utilidades desde el día uno. Qué pasa: apenas ves tres utilidades juntas, sientes el impulso de "limpiarlas" en una clase .card con @apply. Por qué pasa: años de CSS semántico te enseñaron que "estilos en el marcado = deuda que hay que refactorizar". Cómo detectarlo: tu CSS vuelve a llenarse de clases con nombre de componente, y el marcado vuelve a estar "limpio" pero acoplado. Cómo corregirlo: la ventaja de utility-first es no tener que nombrar todo. @apply existe (lección 7), pero es la excepción para combinaciones que se repiten idénticas muchas veces —no la regla—. Nombrar cada combinación te devuelve a la caja de piezas custom del inicio del módulo. Deja las utilidades en el marcado hasta que la repetición duela; recién ahí extrae.

Meter colores crudos en utilidades arbitrarias en vez de tus tokens. Qué pasa: necesitas el azul de marca y escribes bg-[#3b82f6] (una utilidad "arbitraria" con el valor entre corchetes). Por qué pasa: es más rápido que configurar el token, y "se ve igual". Cómo detectarlo: tu marcado tiene hex sueltos (bg-[#3b82f6], text-[#111]) en vez de nombres de rol (bg-primary, text-text). Cómo corregirlo: bg-[#3b82f6] es el drift del módulo 1 vestido de utilidad —un valor crudo, copiado, que no cambia con el theming ni con un rebranding—. La forma correcta es mapear tu token en la config (lección 6) y usar bg-primary, que apunta a var(--color-primary) y hereda claro/oscuro solo. La utilidad debe referenciar el token, nunca contener el valor.

Ejercicios

Ejercicio 1 — Utilidad o clase semántica. Para cada fragmento, di si describe una utilidad (un ladrillo de una función, reutilizable) o una clase semántica (una pieza con nombre, moldeada a un componente):

  • (a) .p-4 { padding: 1rem; }
  • (b) .product-card { display: flex; gap: 0.5rem; padding: 1rem; border-radius: 0.375rem; }
  • (c) .flex { display: flex; }
  • (d) .hero-banner { ... 18 declaraciones ... }
Ver solución
  • (a) Utilidad. Una clase, una declaración, sin nombre de componente. .p-4 es un ladrillo: hace padding: 1rem lo uses donde lo uses. Reutilizable en cualquier elemento.
  • (b) Clase semántica. Su nombre (product-card) dice para qué sirve, no qué hace, y agrupa varias declaraciones moldeadas a esa pieza. Es la pieza custom con etiqueta.
  • (c) Utilidad. Una clase, una declaración (display: flex), nombre por lo que hace. Otro ladrillo.
  • (d) Clase semántica. Nombre por su rol en la página (hero-banner) y muchas declaraciones adentro. Pieza custom.

La pregunta guía: ¿el nombre dice qué hace (utilidad) o para qué sirve en esta pantalla (semántica)? ¿Una declaración (utilidad) o muchas (semántica)?

Ejercicio 2 — Predice la salida. Sin correr nada, di qué imprimiría el ejemplo trabajado si cambiáramos el bucle a estas utilidades:

for (const cls of ['gap-2', 'p-8']) {
Ver solución

Imprimiría las dos clases con sus declaraciones, leyendo la escala de espaciado (gap-2 = 2 × 0.25rem = 0.5rem, p-8 = 8 × 0.25rem = 2rem):

  .gap-2      { gap: 0.5rem; }
  .p-8        { padding: 2rem; }

La razón es la misma del ejemplo: cada utilidad es una clase de una declaración, y su valor sale de la escala (spacing[step]). p-8 no es "más grande arbitrariamente"; es exactamente el paso 8 de una base de 0.25rem. Esa regularidad es lo que el módulo 4 llama coherencia por la escala.

Ejercicio 3 — Ubica la pieza en su capa. El módulo 1 te dio las cuatro capas (tokens → utilidades → componentes → patrones). Para cada afirmación sobre este módulo 3, di si es verdadera o falsa y por qué:

  • (a) "Este módulo construye la capa de tokens."
  • (b) "Una utilidad puede referenciar un token."
  • (c) "Tailwind reemplaza a los tokens del módulo 2."
Ver solución
  • (a) Falsa. La capa de tokens fue el módulo 2. Este módulo construye la capa 2: las utilidades, que se apoyan sobre los tokens. Aquí las utilidades consumen tokens; no los definen.
  • (b) Verdadera. Es el corazón de la conexión entre capas: bg-primary no lleva un color crudo, lleva background-color: var(--color-primary) —una referencia al token que definiste en el módulo 2—. La lección 6 configura exactamente eso.
  • (c) Falsa. Tailwind consume tus tokens, no los reemplaza. Configuras Tailwind con tu set de tokens (lección 6) para que bg-primary apunte a --color-primary. Sin tokens, bg-primary no tendría a qué apuntar. Las dos capas trabajan juntas: tokens abajo, utilidades encima.

Resumen y siguiente paso

En esta lección instalaste la tesis del módulo 3: una utilidad es una clase de CSS con una sola declaración, y construyes la UI combinándolas en el marcado en vez de escribir clases semánticas a mano. Con los ladrillos LEGO viste la distinción que estructura todo el módulo: las utilidades son piezas estándar de una función que compones (p-4, flex), el CSS semántico es mandar a fabricar una pieza custom con nombre cada vez (.product-card), y @apply es pegar los ladrillos que siempre van juntos. Y lo comprobaste ejecutando: util('p-4') devolvió padding: 1rem —una clase, una declaración, un ladrillo—.

Antes de avanzar deberías poder: explicar qué es una utilidad y en qué se diferencia de una clase semántica; decir por qué .p-4 aparece una sola vez en el CSS aunque mil elementos la usen; y ubicar este módulo como la capa de utilidades que se apoya sobre los tokens del módulo 2.

La lección 2 baja el primer escalón: qué significa exactamente utility-first. Vas a ver la mecánica de estilar componiendo utilidades directo en el marcado —tomar un product-card sin estilo y vestirlo clase por clase—, y por qué eso, que a primera vista parece desordenado, es en realidad un sistema. Es la definición formal del modelo que aquí conociste de forma intuitiva.

Recursos

  • Tailwind CSS, "Utility-First Fundamentals" — tailwindcss.com/docs/styling-with-utility-classes. La página oficial que introduce el modelo de este módulo: estilar componiendo utilidades. Léela como el mapa oficial de lo que veremos a fondo. En inglés.
  • Adam Wathan, "CSS Utility Classes and Separation of Concerns" — adamwathan.me/css-utility-classes-and-separation-of-concerns. El ensayo del creador de Tailwind que argumenta por qué utility-first; el fundamento conceptual de la lección 3. En inglés.
  • MDN, "Using CSS custom properties (variables)" — developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties. El vehículo del token que las utilidades referenciarán (var(--color-primary)); repásalo porque la lección 6 lo usa. En inglés.
  • web-fundamentals-html-css — módulo 3, "La cascada y la especificidad". El prerequisito que la lección 5 conecta: por qué las utilidades ganan por orden de fuente. Tenlo a mano.