Módulo 3: Utility First With Tailwind

Las utilidades y la cascada

Descripción

Esta es la lección que conecta el módulo con web-fundamentals. La lección 4 dejó una pregunta abierta: cuando dos utilidades tocan la misma propiedad y caen en el mismo elemento —<div class="p-8 p-4">, con dos paddings—, ¿cuál gana? El instinto dice "la última del class", como si fueran instrucciones que se pisan en orden de escritura. El instinto se equivoca, y entender por qué es entender cómo las utilidades viven dentro de la cascada.

La respuesta tiene dos partes, y las dos vienen directo del módulo 3 de web-fundamentals. Primera: todas las utilidades son clases de una sola clase, así que todas tienen la misma especificidad, [0,1,0]. Ninguna le gana a otra por ser "más específica" —están empatadas—. Segunda: cuando la especificidad empata, la cascada decide por orden de fuente —gana la regla que aparece después en el CSS—. Y el orden en el CSS no es el orden en que escribes las clases en el class; es el orden en que Tailwind puso esas clases en la hoja generada (el orden del catálogo de la lección 4). Por eso el orden del class="" es irrelevante: no importa si escribes p-8 p-4 o p-4 p-8; gana la que Tailwind haya puesto de última en la hoja.

Conexión con el módulo. La lección 4 mostró que las utilidades son CSS normal; esta cobra el cheque: si son CSS normal, obedecen la cascada normal —la que ya sabes—. Reusa specificity y la lógica de resolve de web-fundamentals M3 (no los re-enseñamos; los aplicamos a utilidades). Es una lección bisagra: explica por qué pelear la especificidad de Tailwind con !important es un error (reordena, no fuerces), prepara la lección 6 (los tokens no cambian nada de esto: bg-primary sigue siendo [0,1,0]) y el proyecto (donde resolvemos un conflicto real de padding por orden de fuente).

Una analogía: dos ladrillos del mismo tamaño en la misma ranura

Vuelve a los ladrillos LEGO, pero ahora con una torcedura. Imagina que intentas encajar dos ladrillos del mismo tipo —dos 2×4— en la misma ranura de la base. No caben los dos; solo uno queda puesto. ¿Cuál?

Aquí está la clave: los dos ladrillos son idénticos en rango —mismo tipo, mismo tamaño, ninguno tiene prioridad sobre el otro por ser "más importante"—. Así que la pregunta "¿cuál es más fuerte?" no tiene respuesta: están empatados. Lo único que los diferencia es cuál pusiste de último: el segundo ladrillo desplaza al primero y queda él. No ganó por ser mejor; ganó por llegar después, y solo porque estaban empatados.

Ahora el giro que sorprende: en Tailwind, el "cuál pusiste de último" no es el orden en que escribes las clases en el class. Es el orden en que la fábrica (Tailwind) puso esos ladrillos en el catálogo. Tú, al escribir class="p-8 p-4", no estás "poniendo primero p-8 y luego p-4"; estás diciéndole al navegador "este elemento usa las clases .p-8 y .p-4", y el navegador va a la hoja a ver cuál de las dos reglas está escrita más abajo. Si la fábrica puso .p-4 antes que .p-8 en la hoja (porque 4 va antes que 8 en la escala), entonces .p-8 está más abajo y gana —sin importar que hayas escrito p-4 de último en el class—. El orden de tu mano no cuenta; cuenta el orden del catálogo.

Ejemplo trabajado: p-4 vs p-8, igual especificidad, orden de fuente

Montemos el conflicto exacto. Un elemento tiene class="p-8 p-4" —dos utilidades de padding, con p-4 escrita de última en el class—. En la hoja que Tailwind genera, las reglas de padding salen en orden de escala: .p-4 antes que .p-8 (porque 4 < 8). Vamos a medir dos cosas: que ambas tienen la misma especificidad, y quién gana según la cascada.

Reusamos specificity y cmpVec de web-fundamentals M3 —el mismo código que usaste allá—, y modelamos la hoja generada como una lista ordenada de reglas (cada una con su order, su posición en el CSS):

// L5 — igual especificidad + orden de fuente. specificity/cmpVec son de web-fundamentals M3.
function specificity(selector) {
  let a = 0, b = 0, c = 0;
  const pieces = selector.trim().split(/\s+|>|\+|~/).filter(Boolean);
  for (const piece of pieces) {
    a += (piece.match(/#[\w-]+/g) || []).length;   // ids
    b += (piece.match(/\.[\w-]+/g) || []).length;   // clases
    const type = piece.match(/^[a-zA-Z][\w-]*/);    // tipo
    if (type && type[0] !== '*') c += 1;
  }
  return [a, b, c];
}
function cmpVec(x, y) {
  for (let i = 0; i < 3; i++) { if (x[i] > y[i]) return 1; if (x[i] < y[i]) return -1; }
  return 0;
}

// El CSS que Tailwind GENERA, en su orden fijo (la escala: p-4 antes que p-8).
const generatedCss = [
  { selector: '.p-4', prop: 'padding', value: '1rem', order: 1 },
  { selector: '.p-8', prop: 'padding', value: '2rem', order: 2 },
];

// El elemento las lista en CUALQUIER orden; aqui p-4 va de ULTIMO en el class="".
const elementClasses = ['p-8', 'p-4'];

// resolveProp: entre las reglas aplicables para una propiedad, decide la ganadora.
// Misma logica que resolve() de web-fundamentals M3: especificidad y, si empata, orden de fuente.
function resolveProp(prop, classes, css) {
  const applicable = css.filter((r) => r.prop === prop && classes.includes(r.selector.slice(1)));
  let best = applicable[0];
  for (const r of applicable.slice(1)) {
    const c = cmpVec(specificity(r.selector), specificity(best.selector));
    if (c > 0) best = r;
    else if (c === 0 && r.order > best.order) best = r; // empate -> gana el que aparece DESPUES
  }
  const tied = applicable.some((r) => r !== best &&
    cmpVec(specificity(r.selector), specificity(best.selector)) === 0);
  return { best, why: tied ? 'orden de fuente (misma especificidad)' : 'mayor especificidad' };
}

console.log('class="' + elementClasses.join(' ') + '"   (p-4 escrito de ULTIMO)\n');
console.log('=== especificidad de cada utilidad ===');
for (const r of generatedCss) {
  console.log('  ' + r.selector.padEnd(6) + 'especificidad ' + JSON.stringify(specificity(r.selector)) +
              '   (orden en el CSS: ' + r.order + ')');
}

const { best, why } = resolveProp('padding', elementClasses, generatedCss);
console.log('\n=== quien gana en "padding" ===');
console.log('  gana ' + best.selector + '  ->  padding: ' + best.value);
console.log('  por: ' + why);

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

class="p-8 p-4"   (p-4 escrito de ULTIMO)

=== especificidad de cada utilidad ===
  .p-4  especificidad [0,1,0]   (orden en el CSS: 1)
  .p-8  especificidad [0,1,0]   (orden en el CSS: 2)

=== quien gana en "padding" ===
  gana .p-8  ->  padding: 2rem
  por: orden de fuente (misma especificidad)

Lee la salida como la demostración de las dos partes. La primera sección mide la especificidad: .p-4 es [0,1,0] y .p-8 es [0,1,0]idénticas—. Son las dos clases de una sola clase, así que puntúan igual (un cero en ids, un uno en clases, un cero en tipos). Ninguna es "más específica" que la otra; están empatadas, como los dos ladrillos 2×4 del mismo rango.

La segunda sección resuelve el empate. Como la especificidad no decide, la cascada pasa a su último criterio —el orden de fuente— y gana la regla que aparece después en el CSS generado: .p-8, que está en la posición 2 del catálogo. El resultado es padding: 2rem. Y aquí está lo que hay que grabar: escribí p-4 de último en el class, pero ganó p-8. El orden de mi class="" no tuvo ningún efecto; lo que decidió fue el orden en el CSS, y ahí .p-8 va después de .p-4. Si hubiera escrito class="p-4 p-8", el resultado sería idéntico —seguiría ganando .p-8—, porque el navegador no mira el orden del class, mira el orden de la hoja.

Esto explica una frustración clásica de quien empieza con Tailwind: "puse p-4 p-8 (o p-8 p-4) y siempre me queda el mismo padding, no el que escribo de último". Correcto —y ahora sabes por qué—: reordenar clases en el class no cambia nada, porque la cascada ignora ese orden. Si quieres el padding chico, no reordenes: quita la clase que no quieres. Tener dos utilidades de la misma propiedad en un elemento es el error; la solución es dejar una, no pelear el orden.

Una pregunta que prepara la próxima lección: si bg-primary (que apunta a var(--color-primary)) chocara con bg-surface en el mismo elemento, ¿cambiaría algo de este análisis por el hecho de que usan tokens? (No: .bg-primary y .bg-surface siguen siendo clases de una clase, ambas [0,1,0], y decidirían por orden de fuente igual que .p-4 y .p-8. Los tokens cambian el valor, no la especificidad. La lección 6 lo confirma.)

Profundización: por qué !important es la respuesta equivocada

Cuando alguien no entiende esto, suele "arreglar" un conflicto de utilidades con !important —o con la utilidad ! de Tailwind, que agrega !important a una clase—. Es un error, y ahora tienes el marco para ver por qué.

!important funciona porque salta por encima de la cascada normal: una declaración !important le gana a cualquier declaración sin !important, sin importar especificidad ni orden. Es un martillo. El problema es que, una vez que lo usas para ganar un conflicto, el próximo conflicto contra esa regla también necesitará !important para superarla —y el siguiente, y el siguiente—. Empiezas una carrera armamentista de !important donde cada regla nueva necesita más fuerza bruta que la anterior, y pierdes justo lo que las utilidades te daban: una cascada predecible donde todo está al mismo nivel [0,1,0] y el orden decide con calma.

La forma correcta, cuando dos utilidades chocan, casi nunca es forzar la especificidad; es eliminar el conflicto: no pongas p-4 y p-8 en el mismo elemento. Si necesitas que un padding dependa de una condición, esa lógica —"si es primary usa p-4, si es lg usa p-8"— es trabajo de las variantes (módulo 5), que resuelven qué clase poner sin que dos se peleen. !important no resuelve el conflicto; lo entierra bajo un martillazo que tendrás que dar cada vez más fuerte. La cascada de utilidades es sana porque todo está empatado en especificidad; !important rompe ese empate y con él la salud del sistema.

Vale la pena una nota sobre cómo Tailwind ordena su catálogo, para no dejarlo como magia. Tailwind agrupa las utilidades por tipo y las emite en un orden estable —dentro de una familia como el padding, en orden de escala—, y coloca ciertos grupos después de otros a propósito para que "el más específico en intención" gane. No necesitas memorizar ese orden; necesitas saber que existe, que es fijo, y que es lo que decide —no tu class—. Si dos utilidades de la misma propiedad chocan, el arreglo es no ponerlas juntas, no adivinar el orden del catálogo.

Errores comunes

Creer que el orden en el class="" decide quién gana. Qué pasa: se escribe class="p-8 p-4" esperando que p-4 (la última) gane, y sale p-8. Por qué pasa: el instinto trata las clases como instrucciones que se pisan en orden de escritura. Cómo detectarlo: reordenas clases en el class para cambiar el resultado y no cambia nada. Cómo corregirlo: el navegador no mira el orden del class; mira el orden de las reglas en el CSS generado. Como todas las utilidades tienen la misma especificidad [0,1,0], decide el orden de fuente —el del catálogo, no el tuyo—. Reordenar el class es inútil; para cambiar el resultado, quita la clase que no quieres.

Pelear la especificidad de Tailwind con !important. Qué pasa: para que una utilidad "gane", se le agrega !important (o la variante ! de Tailwind). Por qué pasa: es el reflejo aprendido de "si no gana, súbele la fuerza". Cómo detectarlo: tu CSS empieza a acumular !important y cada regla nueva necesita el suyo para superar a la anterior. Cómo corregirlo: !important salta la cascada y arranca una carrera armamentista —el próximo conflicto necesitará otro !important—. La cascada de utilidades es predecible porque todo está empatado en [0,1,0]; !important rompe ese empate. Si dos utilidades chocan, elimina el conflicto (deja una sola) o usa variantes (módulo 5) para elegir cuál aplicar; no fuerces.

Poner dos utilidades de la misma propiedad y esperar que "se combinen". Qué pasa: se ponen p-4 y p-8 juntas pensando que harán algo intermedio o que la segunda "ajusta" la primera. Por qué pasa: se confunde "aplicar dos clases" con "sumar dos efectos". Cómo detectarlo: tienes utilidades redundantes de la misma propiedad en un elemento y un resultado que no entiendes. Cómo corregirlo: dos utilidades de padding no se combinan; compiten, y una gana (la de orden de fuente mayor). Tener ambas es siempre un error —una es basura que confunde—. Deja solo la que quieres. Si el padding debe variar según una condición, eso es una variante (módulo 5), no dos clases peleando.

Ejercicios

Ejercicio 1 — ¿Cambia algo si reordeno el class? El elemento tiene class="p-8 p-4" y gana .p-8 (padding: 2rem) por orden de fuente. Si lo cambias a class="p-4 p-8", ¿qué padding queda y por qué?

Ver solución

Queda padding: 2rem (gana .p-8) exactamente igual. El orden en el class no influye: el navegador decide por el orden de las reglas en el CSS generado, donde .p-8 sigue apareciendo después de .p-4 (orden de escala). Reordenaste el class, pero la hoja no cambió, así que el resultado tampoco. La lección: para cambiar el resultado, edita las clases (quita una), no su orden.

Ejercicio 2 — Especificidad de una utilidad. Sin correr nada, di la especificidad [a,b,c] de estos selectores y cuál ganaría si ambos aplicaran a un elemento: .bg-primary (orden 40 en el CSS) y .bg-surface (orden 41 en el CSS).

Ver solución

Ambos son un selector de una sola clase, así que ambos son [0,1,0] (cero ids, una clase, cero tipos) —empatados—. Como la especificidad empata, decide el orden de fuente: gana .bg-surface, que está en la posición 41 (después de .bg-primary, en 40). El elemento quedaría con background-color: var(--color-surface). Igual que con p-4/p-8: misma especificidad, gana el orden de fuente. (Y la moraleja es la misma: no pongas bg-primary y bg-surface juntos; deja uno.)

Ejercicio 3 — Predice la salida. Sin correr nada, di qué imprimiría la sección "quien gana en padding" del ejemplo si cambiáramos generatedCss para que .p-8 fuera order: 1 y .p-4 fuera order: 2 (invirtiendo el orden en el CSS), manteniendo elementClasses = ['p-8', 'p-4'].

Ver solución

Ahora .p-4 es la que aparece después en el CSS (orden 2), así que gana ella por orden de fuente:

=== quien gana en "padding" ===
  gana .p-4  ->  padding: 1rem
  por: orden de fuente (misma especificidad)

Esto prueba el punto central: lo que decide es el orden en el CSS generado, no en el class (que no tocamos: sigue ['p-8', 'p-4']). Al invertir el orden del catálogo, cambió el ganador —sin tocar el marcado—. Por eso el orden del class es irrelevante y el del catálogo manda. (En Tailwind real no reordenas el catálogo a mano; el ejercicio solo aísla qué variable decide.)

Resumen y siguiente paso

En esta lección conectaste las utilidades con la cascada de web-fundamentals: todas las utilidades son clases de la misma especificidad [0,1,0], así que cuando dos chocan en la misma propiedad, ninguna gana por especificidad —decide el orden de fuente, y ese es el orden en el CSS generado, no en tu class=""—. Con los dos ladrillos del mismo tamaño en la misma ranura viste la mecánica: empatados en rango, gana el que quedó puesto de último, y ese "último" lo fija la fábrica (el catálogo), no tu mano. Lo mediste ejecutando: p-4 y p-8 ambas [0,1,0], y ganó p-8 por orden de fuente aunque escribí p-4 de último. Y viste por qué !important es la respuesta equivocada —rompe el empate sano y arranca una carrera armamentista—: si dos utilidades chocan, elimina el conflicto o usa variantes, no fuerces.

Antes de avanzar deberías poder: decir la especificidad de cualquier utilidad y por qué todas empatan; explicar por qué reordenar el class no cambia el resultado; y argumentar por qué !important no es la solución a un conflicto de utilidades.

La lección 6 cierra el círculo con el módulo 2: configurar Tailwind con tus tokens. Hasta aquí bg-primary apareció apuntando a var(--color-primary) como por arte de magia; ahora vas a ver el enganche real —cómo theme.extend en la configuración de Tailwind mapea el nombre primary a tu token --color-primary, para que la utilidad bg-primary herede tu theming claro/oscuro sin que toques el marcado—. Es donde las dos capas del sistema, tokens y utilidades, se conectan de verdad.

Recursos