Módulo 3: Utility First With Tailwind
`@apply` y cuándo extraer
Descripción
Todo el módulo predicó una regla: combina utilidades en el marcado y no inventes clases con nombre. Esta lección presenta la única excepción sancionada —@apply— y, más importante, marca con precisión cuándo usarla y cuándo no. @apply es una directiva de Tailwind que te deja "pegar" varias utilidades dentro de una clase de CSS propia: escribes .btn { @apply flex p-4 rounded-md bg-primary; } y esa clase queda con las declaraciones de esas cuatro utilidades. Es útil para agrupar una combinación que se repite idéntica muchas veces —el ejemplo canónico es un botón que aparece en cincuenta lugares con exactamente las mismas ocho utilidades—.
Pero @apply es un arma de doble filo, y esta lección insiste en eso porque es donde más gente se equivoca. Si lo usas por instinto —"esto se ve cargado, lo limpio en una clase"— reconstruyes, ladrillo por ladrillo, el CSS semántico que el módulo entero te enseñó a dejar atrás: vuelves a nombrar todo, tu hoja de estilos vuelve a crecer, y pierdes la velocidad y la ausencia de nombres que eran la ventaja. @apply no es "la forma limpia de usar Tailwind"; es una salida de emergencia para un caso puntual. La regla es: quédate en el marcado hasta que la repetición duela, y solo entonces —con criterio— extrae.
Conexión con el módulo. Esta es la última lección de tema, y es la que evita que apliques mal todo lo anterior. La lección 3 argumentó por qué las utilidades ganan (velocidad, no nombrar, CSS que no crece); @apply mal usado revierte esas tres ventajas, así que esta lección es el guardián de la 3. Reusa el util() de la lección 4 para mostrar qué hace @apply por debajo (inlinea declaraciones). Y deja el terreno listo para el módulo 5: la forma correcta de manejar combinaciones que varían (un botón primary vs secondary) no es @apply, son las variantes —@apply es para lo que se repite idéntico, las variantes para lo que cambia según props—.
Una analogía: soldar los ladrillos que siempre van juntos
Vuelve a los ladrillos LEGO. La gracia de los ladrillos es que se combinan libremente —los tomas sueltos y armas lo que quieras—. Pero imagina que hay una combinación que usas en cada construcción, siempre igual: un 2×4 con una bisagra encima y una teja, que forman "la puerta", y esa puerta idéntica aparece en las cincuenta casas que montas. Armarla ladrillo por ladrillo cincuenta veces es tedioso y propenso a que una salga distinta.
Para ese caso, tiene sentido soldar esos tres ladrillos en una sola pieza y etiquetarla "puerta". Ahora tomas "puerta" de una y la encajas, sin rearmarla cada vez. Eso es @apply: soldar las utilidades que siempre van juntas en una clase con nombre. Es una comodidad legítima cuando la combinación es estable y muy repetida.
Pero mira el peligro, porque es sutil. Si empiezas a soldar cada combinación que ves —"estas dos van juntas aquí, las sueldo; estas tres allá, las sueldo"—, terminas con una caja llena de piezas soldadas custom, cada una con su nombre, usada en uno o dos lugares. Volviste exactamente a la caja de piezas únicas del taller semántico —la que crecía sin control y se llenaba de piezas muertas—. Perdiste la libertad de combinar ladrillos sueltos, que era la razón de usar ladrillos. La pregunta antes de soldar siempre es la misma: ¿esta combinación se repite idéntica tantas veces que soldarla ahorra trabajo de verdad, o la estoy soldando solo porque "se ve cargada"? Lo primero justifica @apply; lo segundo lo prohíbe.
Ejemplo trabajado: qué hace @apply por debajo
@apply no es magia: toma cada utilidad que le nombras, busca sus declaraciones, y las inlinea dentro de tu clase. El resultado es una clase de CSS normal con las declaraciones de todas esas utilidades juntas —literalmente lo que tw() calculó en la lección 4, pero puesto bajo un nombre—.
Modelamos eso con applyToRule(selector, utilities): reusa el util() de la lección 4 y construye la regla CSS que @apply generaría. Lo corremos sobre un botón .btn que aplica las seis utilidades del subconjunto:
// L7 — @apply: inlinea las declaraciones de varias utilidades dentro de UNA regla.
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);
}
// applyToRule(selector, utilities): lo que @apply hace por debajo -> una regla con
// las declaraciones de cada utilidad, en orden.
function applyToRule(selector, utilities) {
const decls = [];
for (const u of utilities) for (const d of util(u)) decls.push(d);
return selector + ' {\n' + decls.map((d) => ' ' + d + ';').join('\n') + '\n}';
}
const utilities = ['flex', 'gap-2', 'p-4', 'rounded-md', 'bg-primary', 'text-lg'];
console.log('/* .btn { @apply ' + utilities.join(' ') + '; } genera: */');
console.log(applyToRule('.btn', utilities));
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
/* .btn { @apply flex gap-2 p-4 rounded-md bg-primary text-lg; } genera: */
.btn {
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 salida y compárala mentalmente con la lección 4. Es el mismo bloque de declaraciones que tw('flex gap-2 p-4 rounded-md bg-primary text-lg') produjo para el botón —siete declaraciones, con bg-primary apuntando a tu token y text-lg como par—. La única diferencia es que ahora viven bajo un nombre, .btn, en tu hoja de estilos, en vez de en el atributo class del marcado. Eso es exactamente lo que @apply hace: mueve las declaraciones de las utilidades del marcado a una clase con nombre. Nada más y nada menos.
Y aquí está el punto que la lección quiere que veas: mira lo que acabas de crear. .btn con siete declaraciones adentro, en una hoja de estilos, con un nombre de componente. ¿Te suena? Es una clase semántica —justo lo que el módulo te enseñó a no escribir—. @apply te devolvió al punto de partida del módulo 1: una clase con nombre y un bloque de CSS. La diferencia es que la escribiste con utilidades en vez de a mano, pero el resultado es idéntico: una pieza custom soldada.
¿Significa que @apply es malo? No —significa que tiene un costo, y el costo es reintroducir todo lo que las utilidades evitaban—: un nombre que inventar y mantener (.btn), una regla que se suma a tu CSS (crece un poco), y la pérdida de leer el estilo en el marcado (ahora hay que ir a buscar qué hace .btn). Ese costo se justifica solo cuando el ahorro es mayor: cuando esa combinación exacta se repite en cincuenta botones y escribirla suelta cincuenta veces sería peor. Para un botón que aparece una o dos veces, el costo de @apply supera al ahorro, y lo correcto es dejar las utilidades en el marcado.
Una pregunta hacia el módulo 5: el botón real de Mercado no es un botón —es un botón que cambia según su variant (primary, secondary, ghost) y su size (sm, md, lg)—. ¿Sirve @apply para eso? (No, o mal: @apply agrupa una combinación fija. Un botón que varía según props necesita elegir distintas utilidades según variant y size —eso son las variantes del módulo 5, no @apply—. @apply es para lo que se repite idéntico; las variantes, para lo que cambia.)
Profundización: la regla de decisión para extraer
¿Cuándo @apply (o extraer a un componente) vale la pena? Una regla de decisión práctica, en orden:
- ¿La combinación se repite idéntica? Si cada aparición tiene utilidades ligeramente distintas (un padding aquí, otro allá), no es una combinación estable —no la extraigas; probablemente sean variantes—.
@applysolo sirve para combinaciones que son exactamente iguales cada vez. - ¿Se repite muchas veces? Dos o tres apariciones no justifican el costo (nombre + regla + indirección). El umbral es difuso, pero piensa en decenas, no en dos. Mientras el número sea bajo, el marcado repetido es más barato que una clase nueva.
- ¿La combinación es estable en el tiempo? Si el diseño de esa pieza todavía está cambiando,
@applyte obliga a editar la clase en vez del marcado, y perdiste agilidad. Extrae cuando el diseño se asentó. - Antes de
@apply, pregunta: ¿esto no es en realidad un componente? La mayoría de las veces, lo que quieres extraer no es una clase CSS sino un componente (de React): un<Button>que encapsula las utilidades en su JSX. Un componente te da lo mismo que@apply(reúso bajo un nombre) más props, variantes y lógica —y es el camino que el módulo 5 toma—.@applytiene sentido sobre todo cuando no puedes crear un componente (HTML plano, contenido de un CMS, estilos de terceros).
La síntesis: prefiere el marcado; si algo se repite idéntico muchas veces y está estable, prefiere un componente; usa @apply solo cuando ni siquiera puedes hacer un componente. @apply es el último recurso, no el primero. Cada vez que lo uses, estás pagando el costo de una clase semántica; que el pago valga la pena.
Errores comunes
Usar @apply para "limpiar" el marcado apenas se ve cargado. Qué pasa: al ver seis utilidades en un elemento, se extraen a una clase con @apply para "ordenar". Por qué pasa: el hábito de que "estilo en el marcado = deuda" dispara la limpieza. Cómo detectarlo: tu hoja de estilos se llena de clases (.card, .btn, .badge) usadas en uno o dos lugares. Cómo corregirlo: "se ve cargado" no es razón para @apply —es el aspecto normal de utility-first, y el marcado cargado es legible—. Extrae solo cuando una combinación idéntica se repite muchas veces. Limpiar por estética te devuelve al CSS semántico y borra las ventajas de la lección 3.
Abusar de @apply hasta reconstruir una hoja de estilos semántica. Qué pasa: casi todo termina en clases con @apply, y el marcado vuelve a estar "limpio" con clases como .product-card, .price, .cta. Por qué pasa: cada extracción individual parece razonable; el problema es acumulativo. Cómo detectarlo: tu CSS volvió a crecer con cada componente y tus clases tienen nombres de rol de UI, no de utilidad. Cómo corregirlo: estás usando Tailwind para escribir CSS semántico, que es lo contrario de para lo que sirve —tienes la complejidad de Tailwind sin sus beneficios—. Da marcha atrás: deja las utilidades en el marcado y reserva @apply para el puñado de combinaciones que de verdad se repiten idénticas en toda la app. Si casi todo está en @apply, no estás haciendo utility-first.
Usar @apply para algo que varía según props (en vez de variantes). Qué pasa: se hace .btn, .btn-secondary, .btn-ghost, .btn-lg con @apply para las variantes de un botón. Por qué pasa: parece la extensión natural de agrupar utilidades. Cómo detectarlo: tienes una familia de clases @apply que se diferencian por un solo aspecto (color, tamaño), y las combinas a mano (class="btn btn-secondary btn-lg"). Cómo corregirlo: eso es exactamente el problema que resuelven las variantes (módulo 5, patrón cva): un componente que, según sus props variant y size, elige la lista de utilidades correcta —sin una explosión de clases @apply que combinar a mano—. @apply es para combinaciones fijas; lo que cambia según props va en variantes.
Ejercicios
Ejercicio 1 — ¿Extraer o no? Para cada caso, di si conviene @apply/componente o dejar las utilidades en el marcado, y por qué:
- (a) Un botón "Add to cart" con las mismas ocho utilidades aparece en 60 tarjetas de producto.
- (b) Un
divcontenedor conflex gap-2 p-4aparece dos veces en toda la app. - (c) Un badge cuyo color y padding cambian según si es "nuevo", "oferta" o "agotado".
Ver solución
- (a) Extraer —idealmente a un componente
<Button>—. Combinación idéntica, repetida decenas de veces, estable. Es el caso canónico: el ahorro (no escribir ocho utilidades 60 veces) supera el costo del nombre. Un componente es mejor que@applyaquí porque además encapsula el marcado del botón. - (b) Dejar en el marcado. Dos apariciones no justifican el costo de una clase nueva. El marcado repetido dos veces es más barato que inventar y mantener
.container. Si más adelante llega a decenas, reconsidera. - (c) Ni
@applyni marcado repetido: variantes (módulo 5). No es una combinación fija que se repite; es una pieza que varía según un estado. Eso es una variante (variant="nuevo|oferta|agotado"), no@apply. Forzarlo con@applydaría una familia de clases que combinar a mano.
Ejercicio 2 — Qué genera @apply. Sin correr nada, escribe la regla CSS que .card { @apply flex p-8 rounded-md bg-surface; } generaría, usando el subconjunto del módulo.
Ver solución
@apply inlinea las declaraciones de cada utilidad en la regla (p-8 = 2rem, bg-surface apunta a su token):
.card {
display: flex;
padding: 2rem;
border-radius: 0.375rem;
background-color: var(--color-surface);
}
Nota que el resultado es una clase semántica normal —.card con cuatro declaraciones—. Eso es lo que @apply produce siempre: una pieza custom con nombre. Que valga la pena depende de cuántas veces se repita idéntica flex p-8 rounded-md bg-surface en tu app.
Ejercicio 3 — Detecta el abuso. Un proyecto tiene esta hoja de estilos. ¿Está usando @apply bien o mal? Justifica.
.product-card { @apply flex gap-2 p-4 rounded-md bg-surface; }
.price { @apply text-lg; }
.cta { @apply flex p-4 rounded-md bg-primary; }
.title { @apply text-lg; }
Ver solución
Mal (abuso). Señales: (1) casi todo está en clases @apply con nombres de rol de UI (product-card, price, title, cta) —es una hoja de estilos semántica reconstruida con Tailwind—; (2) .price y .title aplican una sola utilidad (text-lg), lo que no ahorra nada —envolver una utilidad en una clase con nombre es puro costo, cero beneficio—; (3) no hay evidencia de que estas combinaciones se repitan idénticas muchas veces; parecen extraídas "para limpiar el marcado".
Lo correcto sería dejar estas utilidades en el marcado del product-card (<article class="flex gap-2 p-4 rounded-md bg-surface">, <p class="text-lg">, etc.). Si el botón cta se repite mucho, extraerlo —pero a un componente <Button>, no a una clase—. Este proyecto tiene la complejidad de Tailwind y de una hoja semántica a la vez, sin las ventajas de ninguna.
Resumen y siguiente paso
En esta lección conociste la única excepción a "combina utilidades en el marcado": @apply, que inlinea las declaraciones de varias utilidades dentro de una clase con nombre —produciendo, literalmente, una clase semántica—. Con los ladrillos soldados viste cuándo tiene sentido (una combinación idéntica que se repite muchas veces y está estable, como un botón en cincuenta lugares) y el peligro (soldar cada combinación te devuelve la caja de piezas custom del taller semántico, y pierdes las ventajas de la lección 3). Lo ejecutaste: @apply sobre seis utilidades produjo el mismo bloque que tw(), ahora bajo el nombre .btn —una clase semántica—. Y fijaste la regla de decisión: prefiere el marcado; si algo idéntico se repite mucho y está estable, prefiere un componente; usa @apply solo cuando ni siquiera puedes hacer un componente.
Antes de avanzar deberías poder: explicar qué hace @apply por debajo y por qué el resultado es una clase semántica; dar la regla de decisión para extraer; y distinguir el caso de @apply (combinación fija) del de variantes (lo que cambia según props).
La lección 8 es el proyecto: estilar el product-card de Mercado con utilidades conectadas a tus tokens. Vas a tomar el componente desnudo, vestirlo con utilidades (flex gap-2 p-4 rounded-md bg-surface en la tarjeta, su botón con bg-primary text-lg), y verificar con el modelo en Node dos cosas —que cada utilidad mapea al CSS correcto (tw()), y que cuando dos utilidades de padding chocan, el orden de fuente resuelve el conflicto (no el class)—. Es la síntesis ejecutada de todo el módulo.
Recursos
- Tailwind CSS, "Functions & Directives" (
@apply) — tailwindcss.com/docs/functions-and-directives. La documentación oficial de@apply: qué hace y su sintaxis. En inglés. - Tailwind CSS, "Reusing Styles" — tailwindcss.com/docs/reusing-styles. La guía oficial sobre cuándo extraer, que recomienda componentes antes que
@apply—la misma jerarquía de esta lección—. En inglés. - Adam Wathan, "CSS Utility Classes and Separation of Concerns" — adamwathan.me/css-utility-classes-and-separation-of-concerns. El autor de Tailwind sobre por qué extraer a componentes es preferible a extraer a clases; el fundamento de la regla de decisión. En inglés.
- Tailwind CSS, "Adding custom styles" — tailwindcss.com/docs/adding-custom-styles. El panorama completo de cuándo escribir CSS propio (incluido
@apply) y cuándo no. En inglés.