Módulo 3: Utility First With Tailwind
Qué significa utility-first
Descripción
Utility-first —"utilidades primero"— es una forma de escribir estilos que invierte el hábito que traes de web-fundamentals. Allá aprendiste el flujo clásico: por cada componente, escribes una clase semántica (.product-card) y llenas su bloque de CSS con todas las declaraciones que esa pieza necesita. Utility-first hace lo contrario: no escribes CSS nuevo casi nunca. En su lugar, compones el estilo de cada elemento aplicando muchas utilidades diminutas —cada una con una declaración— directo en el atributo class del marcado. En vez de "una clase con veinte declaraciones", pones "veinte clases con una declaración cada una".
Esta lección es la definición formal del modelo que la presentación te dio de forma intuitiva. Vas a ver la mecánica exacta: tomar un product-card sin estilo y vestirlo utilidad por utilidad, entender qué recibe el elemento al final, y por qué eso —que a primera vista parece meter el CSS en el HTML— es en realidad un sistema disciplinado y no un desorden. La clave está en que las utilidades no son valores sueltos: son clases reutilizables que salen de una escala y pueden apuntar a tus tokens.
Conexión con el módulo. La presentación instaló la tesis (una utilidad es una clase de una declaración) y el util() que la traduce. Esta lección la desarrolla: qué es estilar con utilidades, no solo qué es una utilidad suelta. Es la base de todo lo que sigue —la lección 3 argumenta por qué este modelo gana, la 4 abre la mecánica interna (tw() sobre un componente entero), la 5 explica por qué las utilidades no pelean entre sí, y la 6 las conecta a tus tokens—. Aquí fijamos qué es utility-first; el resto del módulo lo justifica y lo profundiza.
Una analogía: componer la pared con ladrillos, no encargar el panel
Vuelve a la casa de juguete de la presentación, ahora con la escena completa.
El método viejo —CSS semántico— es como llamar al taller y encargar "el panel frontal de la casa" como una pieza única, moldeada, con su etiqueta panel-frontal. Le dictas al taller todo de golpe: las medidas, el hueco de la ventana, el color, el grosor. Cuando llega, encaja perfecto en esa casa. Pero es una pieza y un nombre por cada panel de cada casa: tu inventario crece con cada pantalla.
El método utility-first es abrir tu caja de ladrillos estándar y componer la pared ahí mismo. No encargas nada; tomas los ladrillos que ya tienes y los encajas: un ladrillo flex para que los hijos se acomoden en fila, un gap-2 para separarlos, un p-4 de relleno alrededor, un rounded-md para las esquinas, un bg-surface que pinta el fondo con tu token de superficie. La pared queda armada —y no inventaste ni una pieza nueva, ni le pusiste nombre a nada—. Para la casa vecina reusas los mismos ladrillos.
La objeción natural es "pero ahora la pared tiene los ladrillos a la vista, se ve el mecanismo". Cierto —y es exactamente el punto—. Al ver class="flex gap-2 p-4 rounded-md bg-surface" lees, sin abrir ninguna hoja de estilos aparte, todo lo que este elemento hace. No hay una clase .product-card en otro archivo que tengas que ir a buscar para saber qué padding tiene. El estilo vive donde vive la estructura, y eso —que parece desorden— es lo que hace el marcado legible de un vistazo. La lección 3 convierte esta intuición en argumentos concretos.
Ejemplo trabajado: vestir el product-card, utilidad por utilidad
Tomemos el product-card de Mercado —el mismo componente de react-fundamentals, ahora estilado con el sistema— y vámoslo vistiendo. Empezamos con el marcado desnudo y agregamos una utilidad a la vez, viendo qué hace cada una.
Así se ve el product-card sin una sola utilidad —estructura pura, cero estilo—:
<article class="">
<img src="/shoe.jpg" alt="Zapatilla" />
<h3>Zapatilla urbana</h3>
<p>$1,299</p>
<button>Add to cart</button>
</article>
Ahora lo vestimos. Cada utilidad que agregamos hace una cosa, y la vas leyendo en el class:
<!-- flex: los hijos en columna? no todavia; flex por defecto es fila. Agregamos gap y padding. -->
<article class="flex gap-2 p-4 rounded-md bg-surface">
<img src="/shoe.jpg" alt="Zapatilla" />
<h3 class="text-lg">Zapatilla urbana</h3>
<p class="text-primary">$1,299</p>
<button class="p-4 rounded-md bg-primary">Add to cart</button>
</article>
El article recibe cinco ladrillos: flex (sus hijos se disponen con flexbox), gap-2 (medio rem de separación entre ellos), p-4 (un rem de relleno alrededor), rounded-md (esquinas redondeadas) y bg-surface (fondo con tu token de superficie). El h3 recibe text-lg (tamaño de fuente grande). El precio, text-primary (color de marca). El botón, su propio trío. Nadie escribió una clase .product-card ni .price: la pieza quedó estilada solo componiendo utilidades.
Para ver qué recibe realmente el elemento, modelamos el mapeo utilidad→CSS con Node —lo mismo que Tailwind hace por debajo, con un subconjunto pedagógico—. Le pasamos las clases del article y nos devuelve el bloque de declaraciones que el navegador aplicaría:
// L2 — estilar el article del product-card: sus utilidades -> el CSS que recibe.
const spacing = { '2': '0.5rem', '4': '1rem', '8': '2rem' };
const fontSize = { 'text-lg': ['1.125rem', '1.75rem'] };
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] + ')'];
throw new Error('utilidad fuera del subconjunto: ' + cls);
}
const articleClasses = 'flex gap-2 p-4 rounded-md bg-surface';
console.log('class="' + articleClasses + '"\n');
console.log('el article recibe:');
for (const cls of articleClasses.split(/\s+/)) {
console.log(' .' + cls.padEnd(11) + '-> ' + util(cls).join('; '));
}
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
class="flex gap-2 p-4 rounded-md bg-surface"
el article recibe:
.flex -> display: flex
.gap-2 -> gap: 0.5rem
.p-4 -> padding: 1rem
.rounded-md -> border-radius: 0.375rem
.bg-surface -> background-color: var(--color-surface)
Lee la salida como el "estilo del componente" reconstruido a partir de sus utilidades. El article no tiene una clase con nombre, pero sí tiene un estilo completo: display: flex, gap: 0.5rem, padding: 1rem, border-radius: 0.375rem, y un fondo que apunta a tu token --color-surface. Ese conjunto de cinco declaraciones es exactamente lo que una clase .product-card { ... } habría contenido en el método viejo. La diferencia es dónde vive: en el método viejo, en una hoja aparte, bajo un nombre; en utility-first, en el marcado, como cinco referencias a clases que ya existen.
Fíjate en la última línea, porque es el puente al módulo 2. bg-surface no da #ffffff: da background-color: var(--color-surface). La utilidad referencia tu token, no lo contiene. Eso significa que el mismo bg-surface dará blanco en tema claro y casi negro en tema oscuro, sin que toques el marcado —porque el valor vive en el token, y el token cambia con el tema (módulo 2, lección 6)—. La utilidad es el ladrillo; el token es de qué está hecho; y la lección 6 muestra cómo se enchufan.
Y fíjate en lo que no pasó: no escribiste CSS. No abriste una hoja de estilos, no inventaste una clase, no pensaste un nombre para la tarjeta. Compusiste. Esa es la esencia del first en utility-first: tu primer recurso para estilar es combinar utilidades, no escribir una regla nueva. Escribir CSS propio pasa a ser la excepción —para lo que las utilidades no cubren—, no el punto de partida.
Una pregunta hacia la lección 3: si mañana agregas un wishlist-card que resulta ser visualmente idéntico al product-card —mismo flex gap-2 p-4 rounded-md bg-surface—, ¿cuánto CSS nuevo tienes que escribir? (Cero. Reusas las mismas cinco utilidades en el marcado del nuevo componente; las clases ya existen en el CSS. En el método semántico habrías escrito una clase .wishlist-card con las mismas cinco declaraciones copiadas —drift esperando a pasar—.)
Profundización: utility-first no es "sin CSS", es "sin CSS nuevo casi nunca"
Un malentendido frecuente es pensar que utility-first significa que ya no existe una hoja de estilos. Existe —y es enorme—: es la hoja que Tailwind genera, con miles de clases de una declaración (.p-0, .p-1, … .flex, .grid, .bg-primary, …). Lo que cambia es quién la escribe: la escribe Tailwind, una vez, a partir de tu escala y tus tokens; tú no. Tu trabajo pasa de "escribir declaraciones" a "elegir utilidades". Y como esa hoja generada se descarga una sola vez y las clases se reusan en toda la app, agregar pantallas casi no la hace crecer (la lección 3 lo mide).
Hay un segundo matiz que separa utility-first de los estilos en línea (style="..."), y es crucial para el resto del módulo. Un estilo en línea es un valor único pegado a un elemento, con una especificidad altísima que le gana a casi todo y que no participa en la cascada de forma sana. Una utilidad es una clase —.p-4— con la especificidad normal de una clase: [0,1,0]. Eso significa que las utilidades conviven con el resto de tu CSS bajo las mismas reglas de la cascada que aprendiste en web-fundamentals, y —como todas tienen la misma especificidad— se ordenan entre sí de una forma predecible que la lección 5 desmenuza. Los estilos en línea no te dan eso; las utilidades sí. Por fuera se parecen (ambos ponen el estilo cerca del elemento); por dentro son opuestos.
Errores comunes
Confundir utility-first con estilos en línea (style="..."). Qué pasa: se ve el estilo pegado al elemento y se concluye "es lo mismo que style, con más pasos". Por qué pasa: ambos ponen el estilo en el marcado. Cómo detectarlo: rechazas las utilidades con el argumento "para eso está style". Cómo corregirlo: style="padding:16px" es un valor crudo, único, fuera de la escala, sin theming, con especificidad de estilo en línea (que pelea con todo y obliga a !important). class="p-4" es una clase reutilizable que sale de una escala, puede apuntar a un token, y tiene especificidad de clase normal [0,1,0] —participa en la cascada como cualquier otra clase—. La diferencia se vuelve concreta en la lección 5, cuando veas cómo las utilidades se ordenan entre sí.
Empezar a escribir clases semánticas "porque el marcado se ve cargado". Qué pasa: al ver seis o siete utilidades en un elemento, el instinto dice "esto hay que limpiarlo en una clase .card". Por qué pasa: el hábito de web-fundamentals era que estilo en el marcado = deuda. Cómo detectarlo: empiezas a mover utilidades a clases semánticas en cuanto se acumulan tres. Cómo corregirlo: el marcado "cargado" es el precio —y la ventaja— de tener el estilo visible y sin nombres que inventar. Extraer con @apply es para combinaciones que se repiten idénticas muchas veces (lección 7), no para "limpiar" un elemento que se ve lleno. Un elemento con siete utilidades legibles es más mantenible que una clase .card cuyo contenido tienes que ir a buscar a otro archivo.
Creer que "no escribo CSS" significa "no hay CSS". Qué pasa: alguien piensa que utility-first elimina la hoja de estilos. Por qué pasa: uno deja de escribir declaraciones, así que parece que desaparecieron. Cómo detectarlo: te sorprende que Tailwind genere un archivo CSS, o no entiendes de dónde salen las clases. Cómo corregirlo: la hoja existe y es grande —la genera Tailwind a partir de tu escala y tus tokens—. Tú no la escribes, pero está ahí, y cada class="p-4" es una referencia a una regla real en ella. Entender esto importa para la lección 5 (esas reglas viven en un orden, y ese orden decide conflictos) y para performance (Tailwind purga de esa hoja lo que no usas, otra guía).
Ejercicios
Ejercicio 1 — Traduce de semántico a utility-first. Tienes esta clase semántica. Reescríbela como una lista de utilidades en el atributo class de un div, usando el subconjunto del módulo (flex, gap-N, p-N, rounded-md, bg-<rol>):
.panel {
display: flex;
gap: 0.5rem;
padding: 1rem;
border-radius: 0.375rem;
background-color: var(--color-surface);
}
Ver solución
Cada declaración se vuelve una utilidad, y todas van juntas en el class:
<div class="flex gap-2 p-4 rounded-md bg-surface">…</div>
display: flex→flexgap: 0.5rem→gap-2(2 × 0.25rem)padding: 1rem→p-4(4 × 0.25rem)border-radius: 0.375rem→rounded-mdbackground-color: var(--color-surface)→bg-surface
Cinco declaraciones, cinco utilidades. No inventaste ninguna clase ni pensaste un nombre: compusiste el mismo estilo con ladrillos que ya existen. Y si otro elemento necesita el mismo look, repites las cinco utilidades —sin copiar CSS—.
Ejercicio 2 — ¿Utility-first o estilo en línea? Para cada fragmento, di si es utility-first o estilo en línea, y una consecuencia práctica de la diferencia:
- (a)
<button style="padding: 16px; background: #2563eb"> - (b)
<button class="p-4 bg-primary">
Ver solución
- (a) Estilo en línea. Valores crudos pegados al elemento:
16pxno sale de una escala,#2563ebno es un token. Consecuencia: no cambia con el theming (en tema oscuro seguirá siendo ese azul), y su especificidad de estilo en línea le gana a tu CSS, así que para sobrescribirlo tendrías que pelear con!important. - (b) Utility-first. Dos clases reutilizables:
p-4sale de la escala de espaciado,bg-primaryapunta avar(--color-primary). Consecuencia:bg-primaryda el color correcto en claro y oscuro sin tocar el marcado, y su especificidad es la de una clase normal[0,1,0], así que convive con el resto de tu CSS de forma predecible.
La diferencia no es cosmética: define si tu botón hereda el sistema (theming, escala, cascada sana) o queda fuera de él.
Ejercicio 3 — Predice la salida. Sin correr nada, di qué imprimiría el ejemplo trabajado si cambiáramos la línea de clases del article a:
const articleClasses = 'flex p-8 rounded-md bg-primary';
Ver solución
Imprimiría las cuatro utilidades con sus declaraciones (p-8 = 8 × 0.25rem = 2rem; bg-primary apunta al token de marca):
class="flex p-8 rounded-md bg-primary"
el article recibe:
.flex -> display: flex
.p-8 -> padding: 2rem
.rounded-md -> border-radius: 0.375rem
.bg-primary -> background-color: var(--color-primary)
Nota dos cosas. p-8 da el doble de padding que p-4 porque es el doble de pasos en la misma escala —la escala hace que "más grande" sea una regla, no un valor inventado—. Y bg-primary referencia var(--color-primary), el token de marca del módulo 2, no un hex: la utilidad apunta al token.
Resumen y siguiente paso
En esta lección definiste utility-first: estilar componiendo muchas utilidades diminutas —una declaración cada una— directo en el marcado, en vez de escribir clases semánticas a mano. Con la pared de ladrillos viste la mecánica: vestiste el product-card de Mercado utilidad por utilidad (flex gap-2 p-4 rounded-md bg-surface) sin inventar una sola clase ni un solo nombre, y ejecutaste el mapeo para ver que el article recibe exactamente las cinco declaraciones que una clase .product-card habría contenido —solo que viviendo en el marcado y con bg-surface apuntando a tu token—. Y separaste utility-first de los estilos en línea: por fuera se parecen, por dentro son opuestos (clase reutilizable con escala, token y cascada sana vs valor crudo pegado al elemento).
Antes de avanzar deberías poder: estilar un componente componiendo utilidades sin escribir CSS; explicar por qué utility-first no es lo mismo que style="..."; y decir qué recibe un elemento que tiene cinco utilidades en su class.
La lección 3 responde la pregunta que quedó flotando: ¿por qué utilidades y no clases semánticas? Si el estilo termina siendo el mismo, ¿qué se gana con ponerlo en el marcado en vez de en una clase con nombre? Vas a ver los cuatro argumentos —velocidad, no tener que nombrar todo, un CSS que no crece al agregar pantallas, y consistencia que sale de la escala— y a medir el tercero ejecutando: cuánto CSS agrega una pantalla nueva en cada enfoque.
Recursos
- Tailwind CSS, "Utility-First Fundamentals" — tailwindcss.com/docs/styling-with-utility-classes. La página oficial que define el modelo: estilar componiendo utilidades directo en el marcado, con ejemplos paso a paso. En inglés.
- Adam Wathan, "CSS Utility Classes and Separation of Concerns" — adamwathan.me/css-utility-classes-and-separation-of-concerns. El ensayo que cuenta cómo su autor pasó de rechazar las utilidades a construir Tailwind; la historia detrás de este modelo. En inglés.
- Tailwind CSS, "Adding custom styles" — tailwindcss.com/docs/adding-custom-styles. Qué hacer cuando una utilidad no cubre lo que necesitas —el caso excepción de "escribir CSS propio"—. En inglés.
- MDN, "Using CSS custom properties (variables)" — developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties. El mecanismo de
var(--color-surface)que las utilidades de color referencian; base de la lección 6. En inglés.