Módulo 02: Fundamentos: utilidades y Preflight

Objetivos del módulo

  • Entender qué problema resuelve el enfoque utility-first y qué precio se paga por él
  • Leer cualquier clase de Tailwind separando variante, utilidad, valor, modificador e importancia
  • Saber cómo se genera el CSS: qué lee el escáner, qué descarta el compilador y por qué bg-${color}-500 no funciona
  • Dominar las cuatro capas @layer de Tailwind y predecir quién gana en la cascada
  • Conocer Preflight regla a regla, extenderlo con @layer base y desactivarlo cuando convenga
  • Usar las variables del tema en tu propio CSS y saber cuáles llegan a la hoja final
  • Trabajar con la escala de espaciado --spacing y con los valores dinámicos
  • Escribir valores arbitrarios, propiedades arbitrarias y variables con la sintaxis correcta
  • Distinguir un modificador de una variante y usar /50 o /7 con criterio
  • Forzar una clase con ! y conocer sus alternativas
  • Explicar por qué el orden de las clases en el HTML no decide nada, y detectar los conflictos
  • Hacer convivir Tailwind con otro CSS mediante un prefijo
  • Diagnosticar una clase que no se aplica

1. Preparación del laboratorio

Este módulo se apoya en el proyecto Vite que montaste en el módulo 01. Solo necesitas un index.html y un app.css con esta línea:

@import "tailwindcss";

Arranca el servidor de desarrollo con npm run dev y deja abierto el navegador. Cada vez que el módulo diga “crea una página”, añade un .html a la raíz del proyecto y ábrelo con su ruta (http://localhost:5173/capas.html).

⚠️ Importante: Vite no detecta siempre los ficheros HTML creados con el servidor ya en marcha. Si una clase nueva no aparece, guarda app.css sin cambiar nada: eso obliga a Tailwind a volver a escanear el proyecto.

Para que las figuras sean legibles, los ejemplos usan clases que aún no has estudiado. Con esta tabla te basta por ahora:

Clases Qué hacen Dónde se estudian
p-4, mt-2, px-4, w-32, max-w-xs Relleno, margen y tamaño Espaciado y tamaño
text-sm, font-bold, uppercase, tracking-wider Texto Tipografía
bg-indigo-600, text-gray-900 Color Color y fondos
border, rounded-lg Bordes Bordes, sombras y efectos
flex, grid, grid-cols-3, gap-4, items-center Colocación Flexbox y Grid
hover:, md:, dark: Aplican una clase en una condición Variantes y estados y Diseño responsive

Si necesitas repasar la cascada, la especificidad o las variables CSS, tienes el Curso de HTML y CSS.


2. La filosofía: por qué utilidades

El problema que intenta resolver

La forma clásica de escribir CSS es inventar un nombre que describa qué es un elemento y dar los estilos en otro fichero:

<article class="tarjeta-producto">
  <p class="tarjeta-producto__categoria">Portátiles</p>
  <h2 class="tarjeta-producto__nombre">Portátil UltraBook 14</h2>
  <p class="tarjeta-producto__descripcion">Portátil ligero de 14 pulgadas, 16 GB RAM</p>
  <p class="tarjeta-producto__precio">899 €</p>
</article>
.tarjeta-producto {
  max-width: 20rem;
  padding: 1.5rem;
  border-radius: 0.75rem;
  background-color: #fff;
  border: 1px solid #e5e7eb;
}

.tarjeta-producto__categoria {
  font-size: 0.75rem;
  line-height: 1rem;
  font-weight: 600;
  color: #4f46e5;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.tarjeta-producto__nombre {
  margin-top: 0.25rem;
  font-size: 1.25rem;
  line-height: 1.75rem;
  font-weight: 700;
  color: #111827;
}

.tarjeta-producto__descripcion {
  margin-top: 0.5rem;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: #4b5563;
}

.tarjeta-producto__precio {
  margin-top: 1rem;
  font-size: 1.5rem;
  line-height: 2rem;
  font-weight: 700;
  color: #111827;
}

Es un buen CSS. En un proyecto pequeño funciona muy bien. Los problemas aparecen cuando el proyecto crece y lo tocan varias personas durante años:

  • Los nombres cuestan. Hay que decidir si esto es una tarjeta-producto, un producto o un item-catalogo, y qué hacer cuando aparece una tarjeta casi igual para los empleados.
  • El CSS solo crece. Antes de borrar una regla hay que estar seguro de que nadie la usa, cosa difícil de comprobar. Por eso casi nadie borra, y la hoja engorda con cada función nueva.
  • Los cambios asustan. Modificar .tarjeta-producto afecta a todas las páginas que la usan, incluidas las que no estás mirando.
  • Los valores se dispersan. Sin disciplina acabas con #4f46e5, #4f47e6 y #5046e4, o con márgenes de 13px, 14px y 15px.
  • El contexto se pierde. Para saber cómo se ve un elemento hay que saltar entre dos ficheros.

La misma tarjeta con utilidades

Utility-first significa construir la interfaz componiendo clases pequeñas, cada una con un solo propósito:

<article class="max-w-xs rounded-xl border border-gray-200 bg-white p-6">
  <p class="text-xs font-semibold tracking-wider text-indigo-600 uppercase">Portátiles</p>
  <h2 class="mt-1 text-xl font-bold text-gray-900">Portátil UltraBook 14</h2>
  <p class="mt-2 text-sm text-gray-600">Portátil ligero de 14 pulgadas, 16 GB RAM</p>
  <p class="mt-4 text-2xl font-bold text-gray-900">899 €</p>
</article>

No hay CSS propio. El resultado es prácticamente el mismo:

Dos tarjetas de producto casi idénticas, una con CSS semántico y otra con utilidades de Tailwind

Figura 1. La misma tarjeta escrita de las dos formas. Las diferencias de color son de un tono: la de la izquierda usa hexadecimales, la de la derecha la paleta oklch de Tailwind.

📘 Concepto: Una clase de utilidad hace una sola cosa y su nombre describe qué aspecto da, no qué es el elemento. text-gray-900 no dice “esto es un precio”, dice “este texto es gris muy oscuro”. Una clase semántica como .tarjeta-producto__precio hace lo contrario.

Qué se gana

  • No hay que inventar nombres para cada div.
  • El CSS deja de crecer con el proyecto. Una página nueva reutiliza p-4 y text-sm, que ya están en la hoja. Lo que crece es el HTML, y cuando borras un HTML sus clases desaparecen solas del CSS si nadie más las usa.
  • Los cambios son locales. Tocar las clases de este botón no rompe ningún otro botón.
  • El diseño tiene restricciones. p-4 y p-5 son valores de una escala, no una cifra cualquiera. Los colores salen de una paleta. Un equipo entero acaba usando los mismos valores sin esfuerzo.
  • Todo está a la vista. Lees el HTML y sabes cómo se ve el elemento.

Qué se paga

Tailwind recibe críticas razonables y conviene conocerlas:

Crítica Lo que tiene de verdad Cómo se mitiga
“El HTML queda ilegible” Es cierto: una tarjeta real puede tener 15 clases en un elemento Extraer componentes (React, Angular, Vue…) y ordenar las clases con Prettier
“Repites las mismas clases” También: si copias el botón 20 veces en HTML puro, repites 20 veces Un componente o un bucle. Se ve en Reutilización y componentes
“Mezcla contenido y presentación” Sí, desde el punto de vista del HTML En la práctica, cambiar el aspecto de un sitio casi siempre implica tocar también su estructura
“Hay que aprender otro vocabulario” Sí, aunque mt-4 es margin-top y no otra cosa Saber CSS es requisito: Tailwind no lo sustituye
“Con el CSS del navegador ya puedo hacer todo” Cierto: Tailwind no da nada que CSS no pueda hacer Lo que aporta es organización y restricciones, no capacidades nuevas

💡 Consejo: Utility-first no significa utility-only. Si un patrón se repite y no tienes un framework de componentes, escribir .boton en @layer components es correcto. Tailwind da el sitio exacto para hacerlo, como verás en la sección 5.

“Pero esto es lo mismo que style=""”

Es la primera objeción de casi todo el mundo, y es falsa por cuatro motivos:

  style="..." Clases de utilidad
Valores Cualquiera: padding: 13px Los de la escala y el tema
Estados (:hover, :focus) Imposible hover:bg-indigo-700
Responsive (@media) Imposible md:p-8
Especificidad Muy alta: difícil de sobrescribir Una sola clase, sobrescribible
Tamaño Se repite el texto completo en cada elemento Cada regla está una sola vez en el CSS

Una utilidad se parece a un estilo en línea en dónde se escribe, no en lo que hace.


3. Anatomía de una clase

Toda clase de Tailwind tiene una gramática. Si la aprendes, puedes leer cualquiera aunque nunca la hayas visto:

Clase md:hover:bg-indigo-600/50! separada en variantes, utilidad, valor, modificador e importante

Figura 2. Las cinco partes de una clase. Solo la utilidad es obligatoria.

Parte Ejemplo Qué hace Sección
Variantes md:, hover: Condición en la que se aplica Módulos 10 y 11
Utilidad bg-, p-, flex Qué propiedad CSS se toca Sección 3
Valor indigo-600, 4 Con qué valor Secciones 7 a 10
Modificador /50 Ajusta el valor Sección 11
Importante ! Añade !important Sección 12

Utilidades estáticas y funcionales

Hay dos tipos de utilidad:

  • Estáticas: nombre fijo, sin valor. flex, hidden, uppercase, truncate.
  • Funcionales: un nombre y un valor. p-4, bg-red-500, rounded-lg.
/* Estática */
.flex {
  display: flex;
}

/* Funcional: el 4 entra en la cuenta */
.p-4 {
  padding: calc(var(--spacing) * 4);
}

Una clase, una idea

Casi siempre una clase genera una sola declaración CSS. Hay excepciones cuando una idea necesita varias, como truncate (tres propiedades) o text-lg (tamaño y altura de línea):

.text-lg {
  font-size: var(--text-lg);
  line-height: var(--tw-leading, var(--text-lg--line-height));
}

Fíjate en var(--tw-leading, ...): text-lg trae su altura de línea, pero si otra clase define --tw-leading, se usa esa. Por eso text-lg leading-8 funciona. Tailwind se apoya mucho en estas variables internas --tw-* para que las clases colaboren entre sí.

Cómo adivinar el nombre

Los nombres son abreviaturas muy regulares. Con este patrón aciertas la mayoría:

Patrón Significado Ejemplos
m, p margin, padding m-4, p-2
t, r, b, l Lados físicos: arriba, derecha, abajo, izquierda mt-4, pl-2, border-b
x, y Eje horizontal, eje vertical px-4, my-2
s, e Lados lógicos: inicio y fin de línea ms-2, pe-4
w, h, size Ancho, alto, ambos w-32, h-8, size-4
bg, text, border Fondo, texto, borde bg-sky-300

⚠️ Importante: text- es una utilidad con varias caras. text-lg da tamaño, text-red-500 da color y text-center alinea. Tailwind deduce la propiedad por el tipo del valor. Por eso text-lg text-red-500 no es un conflicto: son propiedades distintas.

Valores negativos

Los márgenes y algunas propiedades admiten un guion delante del nombre. Si hay variantes, el guion va después de ellas:

<div class="-mt-4">Sube 1 rem</div>
<div class="md:-mt-4">Sube 1 rem desde el punto de ruptura md</div>
.-mt-4 {
  margin-top: calc(var(--spacing) * -4);
}

No todas las utilidades lo admiten: el relleno (padding) no puede ser negativo en CSS, así que -p-4 no existe.


4. Cómo se genera el CSS 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) hay que decirle al compilador dónde mirar. Si usas la LTS: declara content: ['./src/**/*.{html,js}'] en tailwind.config.js; en v4 la detección es automática y solo se ajusta con @source.

Una idea central: Tailwind no lleva en el navegador ninguna lista de clases. La hoja final solo contiene lo que tu proyecto usa, y se fabrica antes de que el navegador vea nada.

Cinco pasos: tus ficheros, escáner, compilador, ordenación y Lightning CSS, con un ejemplo de candidatos descartados

Figura 3. El recorrido de una clase desde tu HTML hasta la hoja final.

Paso a paso

  1. Tus ficheros. Todo parte de la línea @import "tailwindcss". Desde ahí, Tailwind recorre el proyecto saltándose lo que ignora Git, node_modules y los binarios. Lo viste en el módulo 01.
  2. Escáner. El motor Oxide, escrito en Rust, lee cada fichero como texto plano. No entiende HTML ni JavaScript: busca cadenas que podrían ser clases. Se llaman candidatos.
  3. Compilador. Cada candidato se analiza con la gramática de la sección 3. Si encaja, se genera su regla. Si no (hola, Mesa), se descarta en silencio.
  4. Ordenación. Las reglas se colocan en un orden fijo dentro de @layer utilities, y se añaden solo las variables del tema que se usan.
  5. Lightning CSS. Convierte la sintaxis moderna para los navegadores objetivo, añade prefijos y, al compilar para producción, minifica.

Lo que produce para class="flex p-4 bg-indigo-600", sin Preflight para abreviar:

/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer theme, base, components, utilities;
@layer theme {
  :root, :host {
    --color-indigo-600: oklch(51.1% 0.262 276.966);
    --spacing: 0.25rem;
  }
}
@layer utilities {
  .flex {
    display: flex;
  }
  .bg-indigo-600 {
    background-color: var(--color-indigo-600);
  }
  .p-4 {
    padding: calc(var(--spacing) * 4);
  }
}

Observa tres cosas:

  • Solo están --spacing y --color-indigo-600, no los casi trescientos colores del tema.
  • El orden no es el del atributo class. La sección 13 lo explica.
  • Cada clase aparece una vez aunque la uses en mil elementos.

El escáner lee texto, no entiende código

Esta es la consecuencia práctica más importante del módulo. Mira este fichero:

const color = 'lime';
const a = `bg-${color}-500`;
const b = { sky: 'text-sky-400', pink: 'text-pink-400' };
// comentario con underline y border-fuchsia-700
const c = "mt-[17px]";
export { a, b, c };

Y esta página:

<p style="color: var(--color-teal-600)">table flex</p>

Compilando de verdad estos dos ficheros, esto es lo que hay en el CSS final:

Texto en el fichero ¿Está en el CSS? Por qué
`bg-${color}-500` No La clase bg-lime-500 no aparece escrita entera en ningún sitio
'text-sky-400', 'text-pink-400' Sí Están enteras, aunque el código no las use nunca
underline, border-fuchsia-700 Sí El comentario es texto como otro cualquiera
table, flex Sí Son palabras del contenido de la página, pero también utilidades
mt-[17px] Sí Un valor arbitrario se detecta igual que los demás
var(--color-teal-600) Sí, se añade la variable El escáner también recoge nombres de variables del tema

⚠️ Importante: Las clases deben aparecer enteras en el código fuente. Nunca construyas nombres por partes. Tailwind no ejecuta tu JavaScript, de modo que bg-${color}-500 es para él solo un trozo de texto.

La solución es escribir la clase completa en algún sitio, normalmente en un objeto:

// Mal: el escáner no ve ninguna clase bg-*
const fondo = `bg-${estado}-500`;

// Bien: todas las clases están escritas enteras
const fondos = {
  disponible: 'bg-emerald-500',
  agotado: 'bg-rose-500',
  reservado: 'bg-amber-500',
};
const fondo = fondos[estado];

Este problema se trata a fondo en Tailwind en React, Angular y otros, junto con @source inline().

💡 Consejo: Que una palabra de tu texto genere una regla (table, flex, hidden) no es grave: son bytes de más y la regla no se aplica a nada mientras no esté en un atributo class. Tailwind prefiere detectar de más antes que dejarse una clase.

¿Cuánto ocupa?

Hemos compilado para producción la página del ejemplo anterior y la tienda de la sección 15:

Página CSS minificado Comprimido con gzip
Ejemplo anterior, con unas pocas clases 4,6 kB 1,6 kB
Tienda con tres tarjetas 17,8 kB 4,6 kB

La mayor parte de la primera fila es Preflight. Como la hoja depende de las clases distintas que usas, no del número de elementos, crece muy despacio. Lo medirás con más detalle en Producción y rendimiento.

Una rareza de los nombres: los escapes

Clases como w-1/2 o mt-[17px] contienen caracteres especiales en CSS. En la hoja aparecen escapados:

.w-1\/2 {
  width: calc(1 / 2 * 100%);
}
.mt-\[17px\] {
  margin-top: 17px;
}

No tienes que hacer nada en el HTML: ahí se escriben tal cual. Solo lo notarás si las buscas desde JavaScript, porque document.querySelector('.w-1/2') falla. Usa CSS.escape('w-1/2').


5. Las capas @layer 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) @layer es una directiva de Tailwind que desaparece al compilar, y el orden depende de la posición en el fichero. Si usas la LTS: escribe @tailwind base; @tailwind components; @tailwind utilities; y coloca tu CSS sin capa antes de esas líneas si quieres que las utilidades lo sobrescriban.

Qué es una capa de cascada

@layer es una función del propio CSS. Permite agrupar reglas y fijar el orden de prioridad entre grupos. Lo decisivo es esto:

📘 Concepto: Entre dos capas distintas, la especificidad no cuenta. Gana la regla de la capa que se declaró más tarde, aunque su selector sea mucho más débil.

Es lo que resuelve la vieja guerra de especificidad. Un selector article#oferta.tarjeta.destacada dentro de una capa pierde frente a .bg-sky-300 si esa clase está en una capa posterior.

Las capas de Tailwind

@import "tailwindcss" es un atajo. Equivale a esto:

@layer theme, base, components, utilities;

@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css" layer(utilities);

La primera línea fija el orden: de menos a más prioridad.

Capa Contenido Quién escribe en ella
theme Variables: --color-*, --spacing… Tailwind, a partir de @theme
base Preflight Tailwind y tú, con estilos para etiquetas (h1, a)
components Vacía por defecto Tú, con clases como .boton o .tarjeta
utilities Las clases generadas Tailwind

Pila de prioridad con el atributo style arriba, CSS sin capa, utilities, components, base, theme y los estilos del navegador abajo

Figura 4. Orden de prioridad. Una utilidad vence a cualquier selector de components o base.

La trampa: el CSS sin capa

Una regla fuera de toda capa gana a todas las capas. Es una regla del estándar CSS, no algo de Tailwind, y sorprende porque invierte la intuición de que “las utilidades siempre ganan”.

Vamos a comprobarlo. Crea app-capas.css:

@import "tailwindcss";

@layer base {
  h1 { color: var(--color-red-600); }
}

@layer components {
  .tarjeta-capa { background-color: var(--color-amber-200); }
}

.caja-sin-capa { background-color: var(--color-rose-300); }

/* Selector muy específico, pero dentro de una capa */
@layer components {
  article#oferta.tarjeta-capa.destacada { background-color: var(--color-amber-400); }
}

Y capas.html:

<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <title>Capas</title>
  <link rel="stylesheet" href="/app-capas.css">
</head>
<body class="bg-white p-6">
  <div class="grid grid-cols-2 gap-4 font-mono text-sm">
    <div class="tarjeta-capa rounded-lg p-4">.tarjeta-capa (components)<br>sin utilidad: ámbar</div>
    <div class="tarjeta-capa rounded-lg bg-sky-300 p-4">.tarjeta-capa + bg-sky-300<br>gana la utilidad</div>
    <article id="oferta" class="tarjeta-capa destacada rounded-lg bg-sky-300 p-4">selector con id (components) + bg-sky-300<br>sigue ganando la utilidad</article>
    <div class="caja-sin-capa rounded-lg bg-sky-300 p-4">.caja-sin-capa (sin capa) + bg-sky-300<br>gana el CSS sin capa</div>
    <div class="caja-sin-capa rounded-lg bg-sky-300! p-4">.caja-sin-capa + bg-sky-300!<br>!important invierte: gana la utilidad</div>
    <div class="rounded-lg bg-sky-300 p-4" style="background-color: #bbf7d0">style="..." + bg-sky-300<br>gana el estilo en línea</div>
  </div>
</body>
</html>

Seis cajas: ámbar, azul, azul, rosa, azul y verde, según qué regla gana en cada caso

Figura 5. Seis duelos reales. El color de fondo indica qué regla ha ganado.

Caja Reglas en juego Gana
1 .tarjeta-capa Ella, porque no compite con nadie
2 .tarjeta-capa frente a bg-sky-300 La utilidad: utilities va después de components
3 Selector con id y tres clases frente a bg-sky-300 La utilidad: la especificidad no cuenta entre capas
4 CSS sin capa frente a bg-sky-300 El CSS sin capa
5 CSS sin capa frente a bg-sky-300! La utilidad con !important
6 style="..." frente a bg-sky-300 El atributo style

⚠️ Importante: Si importas una hoja antigua o una librería (un normalize.css, un tema, un datepicker) sin capa, sus reglas vencerán a tus utilidades. Es el error más desconcertante de v4: la clase está en el HTML, está en el CSS y no hace nada. Importa esa hoja dentro de una capa: @import "./libreria.css" layer(components);.

Dónde va cada cosa

@import "tailwindcss";

@layer base {
  /* Etiquetas del documento */
  h1 { font-size: var(--text-3xl); }
}

@layer components {
  /* Clases que agrupan varias decisiones */
  .boton { padding: --spacing(2) --spacing(4); border-radius: var(--radius-lg); }
}

La regla es sencilla: escribe siempre dentro de una capa. Así una utilidad puntual puede ajustar tu componente: <button class="boton px-8">. Es el objetivo de todo el sistema. La sintaxis --spacing(2) se explica en la sección 7. Ten en cuenta que @utility, la forma de crear tus propias utilidades, se verá en Utilidades y plugins propios.

Qué cambia en la LTS

En v3 la directiva @layer existe, pero no llega al navegador. Este CSS:

.antes { color: red; }
@tailwind components;
@tailwind utilities;
@layer components {
  .usada { padding: 1rem; }
  .sinusar { margin: 1rem; }
}
.despues { color: blue; }

Con <div class="usada p-2 text-center"> produce en 3.4.19:

.antes{color:red}.usada{padding:1rem}.p-2{padding:0.5rem}.text-center{text-align:center}.despues{color:blue}

De aquí salen dos diferencias prácticas con v4:

  Tailwind 3.4 Tailwind 4.3
@layer Se resuelve al compilar Capa real de CSS, llega al navegador
CSS sin capa frente a una utilidad Depende de la posición en el fichero y de la especificidad: .antes pierde, .despues gana El CSS sin capa gana siempre
Selector fuerte en components frente a una utilidad Gana el selector fuerte Gana la utilidad
Clases de @layer components que nadie usa Se eliminan (.sinusar no está) Se conservan: es CSS normal y va a la hoja

6. Preflight a fondo

Preflight es la hoja de reinicio de Tailwind. Se carga en la capa base y elimina las diferencias entre navegadores y casi todos los estilos por defecto. Se basa en modern-normalize, con añadidos propios.

Compara un mismo documento HTML, con cero clases, sin Preflight y con él:

<h1>Tienda EMPRESA</h1>
<h2>Portátiles</h2>
<p>Nuestros equipos más vendidos, con <a href="#">envío gratuito</a> y <strong>dos años</strong> de garantía.</p>
<ul>
  <li>Portátil UltraBook 14</li>
  <li>Portátil Gaming 16</li>
</ul>
<ol>
  <li>Elige tu equipo</li>
  <li>Paga y recíbelo</li>
</ol>
<hr>
<img src="/foto.svg" alt="Portátil"> <img src="/foto.svg" alt="Portátil">
<p><button>Comprar</button> <input placeholder="Tu correo"></p>

Comparación de la misma página: a la izquierda con títulos grandes, listas con viñetas y botón clásico; a la derecha todo con el mismo tamaño y sin márgenes

Figura 6. El mismo HTML. Con Preflight, el navegador deja de opinar sobre el aspecto.

La primera reacción es pensar que está roto. Es intencionado: si cada h1 tuviera su tamaño por defecto, tendrías que anularlo cada vez. Con Preflight, un elemento solo tiene el aspecto que le des con clases, y eso permite elegir la etiqueta por su significado y no por cómo se ve. Un <h3> puede ser enorme y un <h1>, discreto.

Las medidas reales

Medido con getComputedStyle en Chromium:

Elemento Propiedad Sin Preflight Con Preflight
h1 font-size 32 px 16 px
h1 font-weight 700 400
h1 margin 21,44 px arriba y abajo 0
ul margin 16 px arriba y abajo 0
ul list-style-type disc none
img display inline block
button font-size 13,33 px 16 px

Regla a regla

Estas son las reglas de Tailwind 4.3.3 que más te afectan. El fichero completo tiene unas 400 líneas y lo tienes en node_modules/tailwindcss/preflight.css. Es muy legible, con comentarios que explican cada línea.

Caja universal

*,
::after,
::before,
::backdrop,
::file-selector-button {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  border: 0 solid;
}
  • box-sizing: border-box: w-32 mide 8 rem incluyendo relleno y borde, algo mucho más predecible.
  • margin: 0 y padding: 0: ningún elemento trae espacios de serie.
  • border: 0 solid: el borde tiene ancho 0 pero estilo solid. Por eso la clase border solo necesita dar el ancho.

Documento

html,
:host {
  line-height: 1.5;
  -webkit-text-size-adjust: 100%;
  tab-size: 4;
  font-family: --theme(--default-font-family, ...);
  font-feature-settings: --theme(--default-font-feature-settings, normal);
  font-variation-settings: --theme(--default-font-variation-settings, normal);
  -webkit-tap-highlight-color: transparent;
}

Fija una altura de línea legible y la familia tipográfica del tema. --theme() lee una variable del tema con un valor por defecto: si cambias --default-font-family, cambias la fuente de todo el documento.

Títulos, enlaces y listas

h1, h2, h3, h4, h5, h6 {
  font-size: inherit;
  font-weight: inherit;
}

a {
  color: inherit;
  -webkit-text-decoration: inherit;
  text-decoration: inherit;
}

ol, ul, menu {
  list-style: none;
}

Los títulos heredan el tamaño y el peso del padre, los enlaces no son azules ni subrayados y las listas no llevan viñetas.

⚠️ Importante: Los enlaces de la figura 6 no se distinguen del texto. Si no les pones estilo, el usuario no sabrá que son enlaces. Es un olvido de accesibilidad muy frecuente con Tailwind.

💡 Consejo: Safari con VoiceOver deja de anunciar como lista un <ul> con list-style: none. Si es una lista de contenido real, añade role="list" al elemento.

Las viñetas se recuperan con list-disc y list-inside, como verás en el módulo de Tipografía.

Imágenes y contenido reemplazado

img, svg, video, canvas, audio, iframe, embed, object {
  display: block;
  vertical-align: middle;
}

img, video {
  max-width: 100%;
  height: auto;
}

display: block evita el pequeño hueco bajo las imágenes en línea, pero tiene un efecto que sorprende: en la figura 6, las dos imágenes del HTML ya no van seguidas, sino una debajo de otra. Si las quieres en línea, usa inline-block o colócalas con flex.

max-width: 100% y height: auto hacen que las imágenes nunca se salgan de su contenedor.

Controles de formulario

button,
input,
select,
optgroup,
textarea,
::file-selector-button {
  font: inherit;
  font-feature-settings: inherit;
  font-variation-settings: inherit;
  letter-spacing: inherit;
  color: inherit;
  border-radius: 0;
  background-color: transparent;
  opacity: 1;
}

Un <button> sin clases deja de parecer un botón: no tiene fondo, ni borde, ni relleno. En la figura 6 es solo la palabra “Comprar”. También un <input> pierde su borde, así que el campo es invisible hasta que le das border. Los formularios se tratan en Formularios y tablas.

El atributo hidden

[hidden]:where(:not([hidden='until-found'])) {
  display: none !important;
}

Cualquier elemento con el atributo hidden queda oculto incluso si tiene class="flex". Es útil porque en el CSS normal display: flex anularía el hidden.

Diferencias con Preflight de v3 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) el borde es gris claro, el placeholder es gris 400 y los botones llevan cursor: pointer; v4 cambia los tres sin renombrar nada. Si usas la LTS: no tienes que hacer nada; al pasar a v4, indícalos con clases o en @layer base.

Elemento Tailwind 3.4 Tailwind 4.3
Color de border sin más clases gray-200 currentColor
Color del placeholder gray-400 Color del texto al 50 %
Cursor de <button> pointer default, el del navegador

Medido con Chromium en v4: el cursor de un botón es default y el color del placeholder sale como oklab(0 0 0 / 0.5), negro al 50 %.

Los tres cambios tienen un porqué. El borde gris obligaba a anularlo muchas veces. Y el navegador usa default en los botones, de modo que v4 se alinea con él. Si quieres el comportamiento de v3 en un proyecto de v4:

@layer base {
  *,
  ::after,
  ::before,
  ::backdrop,
  ::file-selector-button {
    border-color: var(--color-gray-200, currentColor);
  }

  ::placeholder {
    color: var(--color-gray-400);
  }

  button:not(:disabled) {
    cursor: pointer;
  }
}

Ampliar Preflight con @layer base

Si todos tus títulos o enlaces van a tener el mismo aspecto, no repitas clases: añade una regla base. Es lo correcto para estilos de etiquetas:

@import "tailwindcss";

@layer base {
  h1 {
    font-size: var(--text-2xl);
    font-weight: var(--font-weight-bold);
    color: var(--color-gray-900);
  }

  a {
    color: var(--color-marca-600);
    text-decoration: underline;
    text-underline-offset: 2px;
  }
}

⚠️ Importante: Escríbelo dentro de @layer base. Si dejas h1 { font-size: ... } sin capa, ganará a tus utilidades y <h1 class="text-4xl"> no surtirá efecto, por lo que has visto en la sección 5.

Desactivar Preflight 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) Preflight se desactiva en la configuración; en v4 corePlugins ya no existe y basta con no importarlo. Si usas la LTS: añade corePlugins: { preflight: false } a tailwind.config.js.

Cuando añades Tailwind a un sitio que ya existe, Preflight puede romper lo anterior. Sustituye @import "tailwindcss" por sus piezas, sin la de Preflight:

@layer theme, base, components, utilities;

@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);

Este es exactamente el fichero que genera la columna izquierda de la figura 6.

⚠️ Importante: Sin Preflight, las medidas cambian. Vuelve box-sizing: content-box, y w-32 p-4 border mide 162 px en lugar de 128 (128 + 32 de relleno + 2 de borde). Añade box-border donde lo necesites o una regla propia con box-sizing: border-box. El borde sigue dibujándose en v4, porque border trae su propio estilo mediante --tw-border-style; en la LTS no, porque allí solo da el ancho y hace falta border-solid.

No se puede quitar una sola regla de Preflight. Si una te molesta, sobrescríbela en @layer base, que se carga después.


7. Las variables del tema 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) el tema es un objeto JavaScript y no genera variables CSS. Si usas la LTS: define los valores en theme.extend de tailwind.config.js y léelos en tu CSS con theme(colors.red.500), que se sustituye por el valor al compilar.

En v4 el tema vive en CSS. El fichero node_modules/tailwindcss/theme.css tiene poco más de 500 líneas y empieza así:

@theme default {
  --font-sans:
    -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
    "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
    "Segoe UI Symbol", "Noto Color Emoji";
  --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
  --font-mono:
    ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
    "Courier New", monospace;

  --color-red-50: oklch(97.1% 0.013 17.38);
  --color-red-100: oklch(93.6% 0.032 17.717);
  /* ... */
}

@theme no es solo una lista de variables. Cada variable que declara crea utilidades:

@import "tailwindcss";

@theme {
  --color-marca: oklch(0.55 0.2 270);
}

Con esto, ya existen bg-marca, text-marca, border-marca y todas las demás utilidades de color. Y aparece la variable:

@layer theme {
  :root, :host {
    --color-marca: oklch(0.55 0.2 270);
  }
}
@layer utilities {
  .bg-marca {
    background-color: var(--color-marca);
  }
}

Los espacios de nombres

El prefijo de la variable decide a qué utilidades alimenta:

Variable Utilidades que alimenta Ejemplo
--color-* bg-, text-, border-, ring-… --color-red-500 → bg-red-500
--font-* font- (familia) --font-mono → font-mono
--text-* text- (tamaño) --text-lg → text-lg
--font-weight-* font- (peso) --font-weight-bold → font-bold
--tracking-* tracking- --tracking-wide
--leading-* leading- --leading-loose
--breakpoint-* Variantes sm:, md:… --breakpoint-md: 48rem
--container-* max-w-, @md:… --container-xs → max-w-xs
--spacing p-, m-, w-, gap-… Sección 8
--radius-* rounded- --radius-lg
--shadow-* shadow- --shadow-md
--blur-* blur- --blur-sm
--ease-*, --animate-* ease-, animate- --animate-spin

Personalizarlos a fondo es el tema de Personalizar el tema. Aquí basta con saber que existen y cómo leerlos.

@theme frente a :root

Parecen lo mismo, pero no lo son. Una variable en :root es una variable CSS normal y no crea utilidades:

:root {
  --color-foo: red;
}

Si escribes bg-foo no ocurre nada: la clase no se genera. Usa @theme cuando quieras una clase, y :root para valores auxiliares que solo vaya a leer tu CSS.

Solo llegan las variables usadas

Tailwind quita del resultado las variables que nadie usa. Esto incluye las que usas tú a mano:

@import "tailwindcss";

.etiqueta {
  color: var(--color-red-500);
}
/* Salida */
:root, :host {
  --color-red-500: oklch(63.7% 0.237 25.331);
}
.etiqueta {
  color: var(--color-red-500);
}

También detecta los nombres en HTML y JavaScript, como viste en la sección 4 con style="color: var(--color-teal-600)". Si construyes el nombre de la variable por partes (`--color-${tono}`) no la verá, igual que con las clases. Para esos casos, @theme static emite todas las variables del bloque aunque nadie las use:

@theme static {
  --color-marca: red;
  --color-otra: blue;
}

Leer el tema desde tu CSS

Tienes tres formas, y las tres están comprobadas en 4.3.3. Lo más natural es var(). Las funciones --spacing(), --alpha() y --theme() cubren los casos en que var() no basta:

@import "tailwindcss";

@theme {
  --color-marca: oklch(0.55 0.2 270);
}

.x {
  color: var(--color-marca);
  margin: --spacing(4);
  padding: --theme(--spacing);
  background: --alpha(var(--color-marca) / 50%);
}

Salida:

.x {
  color: var(--color-marca);
  margin: calc(var(--spacing) * 4);
  padding: var(--spacing);
  background: color-mix(in srgb, oklch(0.55 0.2 270) 50%, transparent);
  @supports (color: color-mix(in lab, red, red)) {
    background: color-mix(in oklab, var(--color-marca) 50%, transparent);
  }
}
Función Uso Equivalente en 3.4
var(--color-red-500) Leer un valor en tiempo de ejecución theme(colors.red.500)
--spacing(4) Un múltiplo de la escala theme(spacing.4)
--alpha(color / 50%) Un color con opacidad theme(colors.red.500 / 50%)
--theme(--color-red-500) Un valor del tema theme(colors.red.500)

La función theme() de v3 sigue funcionando en v4, pero conserva la sintaxis antigua y produce el valor literal, sin var(). En código nuevo, prefiere las formas de arriba.

📘 Concepto: Al ser variables CSS reales, los valores del tema se pueden cambiar en el navegador: dentro de un selector, en un style, con JavaScript. En v3 se resolvían al compilar y desaparecían. Esto abre la puerta a los temas múltiples del módulo de Modo oscuro.


8. La escala de espaciado: --spacing 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) la escala es una lista cerrada de valores en rem, escritos en el CSS final. Si usas la LTS: para cambiarla, redefine theme.spacing o amplíala con theme.extend.spacing en tailwind.config.js; no puedes cambiarla en tiempo de ejecución.

Casi todas las medidas de Tailwind salen de una sola variable:

@theme default {
  --spacing: 0.25rem;
}

Con el tamaño de letra por defecto, 0.25rem son 4 px. Las utilidades multiplican esa unidad:

.p-4 {
  padding: calc(var(--spacing) * 4);
}

Esta fórmula es la misma en p-, m-, w-, h-, gap-, size-, inset-, leading- y otras familias:

Clase Multiplicador rem px
p-0 0 0 0
p-0.5 0,5 0,125 2
p-1 1 0,25 4
p-2 2 0,5 8
p-4 4 1 16
p-6 6 1,5 24
p-8 8 2 32
p-12 12 3 48
p-16 16 4 64
p-px No usa la escala   1 px exacto

💡 Consejo: La cuenta mental es sencilla: el número de la clase por 4 da los píxeles. mt-6 son 24 px y w-32 son 128 px.

Dos detalles de la salida a partir de 4.3.1. El multiplicador 0 da un valor fijo, sin variable, y el multiplicador 1 no necesita calc():

.m-0 {
  margin: 0px;
}
.m-1 {
  margin: var(--spacing);
}

Por qué rem

Si un usuario sube el tamaño de letra del navegador, todo lo medido en rem crece con él, incluidos márgenes y rellenos. La página conserva sus proporciones. Si hubieras escrito padding: 16px, el texto crecería, pero el espacio no.

Cambiar la escala de todo un proyecto

Cambia una variable y se reajusta toda la interfaz:

@theme {
  --spacing: 0.3rem;
}

La salida solo cambia en la variable. Las utilidades siguen siendo iguales:

:root, :host {
  --spacing: 0.3rem;
}
.p-2 {
  padding: calc(var(--spacing) * 2);
}

Cambiarla en una zona

Como es una variable real, también puedes redefinirla en un subárbol. Esta sección usa una escala mayor sin tocar ninguna clase:

<section style="--spacing: 0.35rem">
  <article class="mt-5 max-w-64 rounded-lg border border-slate-300 p-4">
    <h3 class="font-bold">Torre Oficina i5</h3>
    <p class="mt-2 text-sm text-slate-600">Ordenador de sobremesa</p>
    <button class="mt-4 rounded-md bg-orange-600 px-4 py-2 text-sm text-white">Añadir</button>
  </article>
</section>

Dos columnas con barras de w-1 a w-48 y una tarjeta; la columna con spacing 0.35rem es proporcionalmente mayor

Figura 7. Mismas clases, dos valores de --spacing. Las barras miden 192 px (w-48) a la izquierda y 268,8 px a la derecha, y la tarjeta crece también. El tamaño de letra no cambia, porque no depende de esta variable.

Es un recurso útil para una vista “compacta” en una tabla o un panel.

⚠️ Importante: --spacing afecta también a lo que no parece espaciado. max-w-64, size-4, leading-6 y text-base/7 dependen de ella. Pruébalo en toda la interfaz antes de cambiar el valor global.

Valores con nombre

Una variable --spacing-<nombre> crea un valor propio para todas estas utilidades:

@theme {
  --spacing-huge: 5rem;
}
.p-huge {
  padding: var(--spacing-huge);
}

Y si quieres un sistema cerrado, --spacing-*: initial elimina la escala: p-4 y w-4 dejan de generarse. Se estudia en Personalizar el tema.

Escala en v3

En 3.4.19, p-4 genera directamente el valor:

.p-4 {
  padding: 1rem;
}

Y un valor nuevo se añade en la configuración:

// tailwind.config.js
export default {
  content: ['./*.html'],
  theme: {
    extend: {
      spacing: { 18: '4.5rem' },
    },
  },
};

Comprobado en 3.4.19: genera .mt-18 { margin-top: 4.5rem }.


9. Valores dinámicos 🆕 v4.0

🆕 Desde Tailwind 4.0. No existe en la LTS (Tailwind 3.4): cada valor debía estar en una lista, y mt-17 no genera nada. Si usas la LTS: escribe un valor arbitrario, como mt-[4.25rem], o añade el valor en theme.extend.

En v3, el compilador consultaba una lista: si 17 no estaba en theme.spacing, no había clase. En v4 muchas utilidades son funciones: reciben un número, comprueban que es válido y hacen la cuenta. Ya no hay lista.

.mt-17 {
  margin-top: calc(var(--spacing) * 17);
}
.grid-cols-15 {
  grid-template-columns: repeat(15, minmax(0, 1fr));
}
.w-3\/7 {
  width: calc(3 / 7 * 100%);
}
.opacity-37 {
  opacity: 37%;
}
.z-37 {
  z-index: 37;
}

Todo lo que sigue se ha pasado por el comprobador de clases en ambas versiones:

Clase en v4 ¿Existe en 3.4? Equivalente en 3.4
mt-17 No mt-[4.25rem]
mt-1.25 No mt-[0.3125rem]
w-13 No w-[3.25rem]
grid-cols-15 No grid-cols-[repeat(15,minmax(0,1fr))]
w-3/7 No w-[42.857143%]
opacity-37 No opacity-[.37]
z-37 No z-[37]
rotate-37 No rotate-[37deg]
duration-137 No duration-[137ms]
scale-117 No scale-[1.17]
border-3 No border-[3px]
aspect-4/3 No aspect-[4/3]
bg-red-500/37 No bg-red-500/[.37]

Los límites

Dinámico no significa cualquier cosa:

  • Solo múltiplos de 0,25. mt-1.25 y mt-1.75 existen; mt-1.3 no genera nada, ni en v4.
  • No todas las utilidades. text-17 o shadow-17 no tienen sentido: su familia usa nombres (text-lg, shadow-md), no números.
  • Sigue sin haber aviso. Si te equivocas, la clase simplemente no existe.

💡 Consejo: Que mt-17 funcione no significa que debas usarlo. La escala existe para que los valores sean coherentes. Si un diseño lleva mt-16, mt-17 y mt-18, probablemente sean el mismo margen mal medido.


10. Valores arbitrarios

Cuando un valor no está en el tema, lo escribes entre corchetes. Tailwind lo copia al CSS:

<div class="rounded-[10px] bg-[#0f766e] p-3 text-[15px] text-white">
  Color y tamaños de una marca externa
</div>
.rounded-\[10px\] {
  border-radius: 10px;
}
.bg-\[\#0f766e\] {
  background-color: #0f766e;
}
.text-\[15px\] {
  font-size: 15px;
}

Esto funciona igual en v3 y v4. La figura resume todas las formas que estudiaremos:

Ocho ejemplos de valores arbitrarios: color, columnas, variable, sintaxis antigua sin efecto, cálculo, propiedad arbitraria, selector y atributo data

Figura 8. Cada ejemplo muestra su clase encima. La caja de bg-[--marca] no tiene fondo: esa es la sintaxis de v3.

Espacios: el guion bajo

Una clase no puede contener espacios, porque el espacio separa clases. Se escribe _ y Tailwind lo convierte:

<div class="grid grid-cols-[120px_1fr]">...</div>
.grid-cols-\[120px_1fr\] {
  grid-template-columns: 120px 1fr;
}

Dentro de url(), el guion bajo se conserva, porque es normal en las rutas:

.bg-\[url\(\/img\/a_b\.png\)\] {
  background-image: url(/img/a_b.png);
}

Las operaciones de calc() no necesitan guiones bajos: Tailwind añade los espacios que exige CSS.

<div class="w-[calc(100%-3rem)]">Ancho calculado</div>
.w-\[calc\(100\%-3rem\)\] {
  width: calc(100% - 3rem);
}

Variables: () frente a [] 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) la forma corta para variables usa corchetes; en v4 usa paréntesis, y la antigua compila sin error, pero genera CSS no válido. Si usas la LTS: escribe bg-[--marca], que allí produce var(--marca), donde v4 escribe bg-(--marca).

Es uno de los falsos amigos del curso. La misma clase compila en las dos versiones y no avisa:

Clase CSS en 3.4.19 CSS en 4.3.3
bg-[--marca] background-color: var(--marca) background-color: --marca (no válido)
bg-(--marca) No existe background-color: var(--marca)
bg-[var(--marca)] background-color: var(--marca) background-color: var(--marca)

Medido en Chromium: con bg-[--marca] en v4 el fondo es rgba(0, 0, 0, 0), transparente.

<div style="--marca: #7c3aed; --relleno: 1.1rem">
  <div class="rounded bg-(--marca) p-(--relleno) text-white">Variable CSS con paréntesis</div>
</div>

💡 Consejo: Si tu código debe compilar en ambas versiones, usa bg-[var(--marca)]. Es más largo, pero tiene el mismo significado en las dos.

Indicar el tipo

A veces Tailwind no puede deducir qué propiedad quieres. text- admite color y tamaño, y una variable no dice cuál es:

<p class="text-(length:--tamano)">Tamaño</p>
<p class="text-(color:--tono)">Color</p>
<p class="text-[length:var(--tamano)]">Tamaño, con corchetes</p>
.text-\(length\:--tamano\) {
  font-size: var(--tamano);
}
.text-\(color\:--tono\) {
  color: var(--tono);
}

Otra pista frecuente es image, para fondos: bg-(image:--fondo) genera background-image.

Sin pista, Tailwind elige por defecto. text-[var(--x)] genera color, de modo que si tu variable era un tamaño no tendrá efecto.

Propiedades arbitrarias

Si Tailwind no tiene utilidad para una propiedad, escribe la declaración CSS completa entre corchetes:

<div class="[writing-mode:vertical-rl]">Vertical</div>
<div class="[mask-type:luminance]">...</div>
.\[mask-type\:luminance\] {
  mask-type: luminance;
}

Admiten variantes y modificadores: hover:[writing-mode:vertical-rl].

Variantes arbitrarias

También se puede dar un selector propio. & representa al elemento:

<ul class="[&>li]:py-1 [&>li]:ps-2 [&>li:nth-child(even)]:bg-slate-100">
  <li>Ratón inalámbrico</li>
  <li>Teclado mecánico</li>
  <li>Monitor 27</li>
</ul>
.\[\&\>li\]\:py-1 > li {
  padding-block: var(--spacing);
}

Algunas variantes aceptan valores arbitrarios:

<div data-estado="agotado" class="data-[estado=agotado]:opacity-50">Sin stock</div>
.data-\[estado\=agotado\]\:opacity-50[data-estado="agotado"] {
  opacity: 50%;
}

Lo verás con detalle en Variantes y estados.

Cuándo usar valores arbitrarios

Son una salida de emergencia, no una forma normal de trabajar:

Situación Qué hacer
Un valor único en toda la aplicación (el ancho de un banner de 327 px) Valor arbitrario
Una propiedad sin utilidad Propiedad arbitraria
Un color de marca que se repite Variable en @theme, y luego bg-marca
Un valor que ya existe en la escala (p-[16px]) p-4, sin duda
Un valor que calcula JavaScript style o una variable: w-(--ancho)

La última fila merece una explicación. Un valor generado en tiempo de ejecución no puede ir en una clase arbitraria (w-[${ancho}px]), porque el escáner no lo verá. La forma correcta es:

<div class="w-(--ancho)" style="--ancho: 120px"></div>

11. Modificadores

Un modificador va tras una barra / y ajusta el valor de la utilidad. No es lo mismo que una variante: la variante dice cuándo, el modificador dice cómo.

hover:bg-indigo-600/50
└ variante        └ modificador

Opacidad del color 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) el modificador genera rgb(239 68 68 / 0.5) y solo admite múltiplos de 5; v4 usa color-mix() y acepta cualquier número. Si usas la LTS: la clase se escribe igual, y para otro valor usa corchetes: /37 pasa a ser /[.37].

Es el modificador más común. Funciona en cualquier utilidad de color:

<div class="bg-indigo-600/50 text-white">Fondo al 50 %</div>
<div class="border-2 border-rose-500/30">Borde al 30 %</div>
.bg-red-500\/50 {
  background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 50%, transparent);
  @supports (color: color-mix(in lab, red, red)) {
    background-color: color-mix(in oklab, var(--color-red-500) 50%, transparent);
  }
}

Hay dos declaraciones por compatibilidad: la segunda, con oklab, da mezclas más regulares y la usan los navegadores que la soportan. El módulo de Color y fondos explica color-mix() en detalle.

No confundas el modificador con la utilidad opacity-*:

Muestrario de opacidades del color y comparación con opacity-50, y tres tamaños de altura de línea

Figura 9. Arriba, opacidad del color. En el centro, la diferencia con opacity-50, que aclara también el texto. Abajo, altura de línea con modificador.

  bg-indigo-600/50 opacity-50
Qué se vuelve transparente Solo el fondo Todo el elemento: texto, hijos y bordes
Propiedad background-color opacity
Efecto secundario Ninguno Crea un contexto de apilamiento

Altura de línea en el tamaño de texto

text-* admite un modificador para fijar a la vez la altura de línea:

<p class="text-base/5">Más apretado</p>
<p class="text-base/7">Más aireado</p>
<p class="text-base/loose">Con nombre</p>
.text-lg\/7 {
  font-size: var(--text-lg);
  line-height: calc(var(--spacing) * 7);
}
.text-lg\/loose {
  font-size: var(--text-lg);
  line-height: var(--leading-loose);
}

El número usa la misma escala de espaciado: /7 son 1,75 rem. Se prefiere a text-base leading-7 porque el tamaño y la altura de línea son una sola decisión.

Otros modificadores

Clase Qué ajusta Dónde se estudia
bg-linear-to-r/oklch Espacio de color del degradado Módulo 05
shadow-lg/20 Opacidad de la sombra Módulo 06
group/tarjeta Nombre del grupo Módulo 11

Además se admiten valores arbitrarios en el modificador: bg-red-500/[0.33], text-lg/[1.3]. Y con variables: bg-(--marca)/50.

⚠️ Importante: No toda barra es un modificador. En w-1/2 y aspect-4/3, la barra forma parte del valor: es una fracción.


12. Forzar con ! 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) el signo ! va delante de la utilidad; en v4 va al final de la clase. Si usas la LTS: escribe !flex y hover:!flex, porque allí flex! no se aplica y no da ningún error.

Añadir ! al final de una clase genera !important:

<div class="bg-red-500 bg-blue-500!">Azul</div>
.flex\! {
  display: flex !important;
}

El signo va al final de toda la clase, tras variantes y modificadores:

Clase en v4 Clase en v3 Significado
flex! !flex display: flex !important
hover:flex! hover:!flex Con hover
mt-4! !mt-4 Margen importante
bg-red-500/50! !bg-red-500/50 Con modificador de opacidad

En v4, la forma antigua !flex sigue compilando como clase obsoleta. Al revés no ocurre: en 3.4.19, flex! genera un .flex normal que no coincide con la clase flex! del HTML, y mt-4! no genera nada. No hay error, simplemente no se aplica.

!important invierte las capas

Una declaración importante siempre vence a una normal, sea cual sea su capa. Por eso, en la figura 5, bg-sky-300! gana al CSS sin capa.

Hay además un detalle poco conocido del estándar CSS. Si las dos declaraciones son importantes, el orden de las capas se invierte: gana la de la capa anterior, y las de capa ganan a las de fuera de capa. Así, un !important en @layer base vencería a bg-sky-300!.

Marcar todo como importante 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) la opción está en la configuración; en v4 pasa a la importación. Si usas la LTS: añade important: true a tailwind.config.js.

Si tu CSS convive con otra hoja de la que no puedes prescindir, puedes marcar todas las utilidades:

@import "tailwindcss" important;
.p-4 {
  padding: calc(var(--spacing) * 4) !important;
}

⚠️ Importante: ! es un último recurso. Si necesitas ! en muchas clases, el problema suele ser CSS sin capa o mal colocado. Arregla eso primero: una sobrecarga de !important hará imposible sobrescribir nada más adelante.


13. El orden de las clases

El orden del atributo class no importa

Es una propiedad del propio CSS que mucha gente no tiene clara. Si dos reglas tienen la misma especificidad, gana la última de la hoja de estilos, no la última del atributo class. Estos dos elementos se ven iguales:

<div class="bg-red-500 bg-blue-500">...</div>
<div class="bg-blue-500 bg-red-500">...</div>

Ocho ejemplos del orden de clases: los pares se ven iguales aunque cambie el orden del atributo

Figura 10. Los pares son idénticos: da igual en qué orden se escriban las clases. Solo ! cambia el resultado.

Ambos son rojos. La hoja tiene este orden:

.bg-blue-500 { background-color: var(--color-blue-500); }
.bg-red-500 { background-color: var(--color-red-500); }

Cómo ordena Tailwind

Tailwind fija el orden de su hoja con criterios estables. Esta es la salida real para px-2 p-4 pt-1 m-2 mx-auto flex hidden block text-lg font-bold hover:p-1 md:p-2 focus:p-3 sm:p-5 dark:p-6, en el orden en que salen:

m-2  mx-auto  block  flex  hidden  p-4  px-2  pt-1  text-lg  font-bold
hover:p-1  focus:p-3  sm:p-5  md:p-2  dark:p-6

De ahí salen las reglas que importan:

  1. Primero las utilidades simples, luego las variantes. Así md:p-2 gana a p-4, sin importar dónde lo escribas.
  2. Por grupos de propiedades. Márgenes, display, relleno, tipografía…
  3. Lo general antes que lo particular. p-4 va antes que px-2, y este antes que pt-1. Esto es muy útil: class="p-8 px-2" significa “32 px, salvo los laterales, que son 8 px”.
  4. Puntos de ruptura de menor a mayor. sm: antes que md:.

Los pares amarillos de la figura 10 lo confirman: padding-top es de 32 px y padding-left de 8 px en los dos casos.

Dos clases para la misma propiedad 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) las clases rivales se ordenan como texto; en v4 los números se ordenan por valor, y mt-2 mt-10 cambia de ganador sin aviso. Si usas la LTS: no dependas del orden; deja solo una de las dos clases y obtendrás lo mismo en las dos versiones.

Si escribes dos clases que tocan la misma propiedad (p-2 p-10, flex hidden), es un conflicto y debes evitarlo. Gana una de ellas por un criterio accidental. Comparando las salidas:

4.3.3   .p-2 .p-4 .p-10    .mt-2 .mt-10    .text-center .text-left
3.4.19  .p-10 .p-2 .p-4    .mt-10 .mt-2    .text-left .text-center

v4 ordena los números por su valor; v3, como texto, por lo que 10 va antes que 2. Como gana la última:

Clases Gana en 3.4.19 Gana en 4.3.3
p-2 p-10 p-2 p-10
mt-2 mt-10 mt-2 mt-10
text-center text-left text-center text-left
flex hidden hidden hidden
bg-red-500 bg-blue-500 bg-red-500 bg-red-500

El caso real: clases condicionales

El conflicto casi nunca está escrito a mano. Aparece así:

// Mal: el elemento destacado lleva p-4 y p-8, y p-8 gana por casualidad
const clases = `p-4 ${destacado ? 'p-8' : ''}`;

// Bien: cada caso aporta una sola clase de relleno
const clases = destacado ? 'p-8' : 'p-4';

El caso más difícil es un componente que acepta clases desde fuera. Si la Tarjeta lleva p-4 y le pasas p-8, ¿quién gana? Para eso existe tailwind-merge, que elimina la clase perdedora. Lo verás en Reutilización y componentes.

Ordenar con Prettier

Que el orden no afecte al resultado no significa que dé igual para quien lee. prettier-plugin-tailwindcss, instalado en el módulo 01, ordena las clases siguiendo el mismo criterio del compilador:

<!-- Antes -->
<div class="text-white p-4 hover:bg-indigo-700 bg-indigo-600 md:p-6 flex rounded-lg font-semibold mi-clase mt-2 hover:underline focus:outline-2 sm:flex-col items-center"></div>

<!-- Después de ejecutar Prettier -->
<div
  class="mi-clase mt-2 flex items-center rounded-lg bg-indigo-600 p-4 font-semibold text-white hover:bg-indigo-700 hover:underline focus:outline-2 sm:flex-col md:p-6"
></div>

Las clases propias (mi-clase) van delante y las variantes, al final. Esta salida es real, de Prettier 3.9.10 con el plugin 0.8.1.

💡 Consejo: Como el plugin ordena igual que Tailwind, una clase que se pisa con otra queda al lado de su rival. Las incoherencias saltan a la vista.

Las variantes se apilan 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) las variantes apiladas se aplican de derecha a izquierda; en v4, de izquierda a derecha. Si usas la LTS: invierte las combinaciones con selectores: escribe first:*:pt-0 donde v4 escribe *:first:pt-0.

Puedes encadenar variantes (md:hover:bg-red-500), y la salida anida las condiciones:

@media (width >= 48rem) {
  @media (hover: hover) {
    .md\:hover\:bg-red-500:hover {
      background-color: var(--color-red-500);
    }
  }
}

En casi todas las combinaciones el orden es indiferente. Importa con selectores como *: o first:, y lo estudiarás en Variantes y estados, junto con el @media (hover: hover) que ves aquí.


14. Convivir con otro CSS: el prefijo 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) el prefijo va pegado a la utilidad; en v4 funciona como una variante al inicio de la clase. Si usas la LTS: añade prefix: 'tw-' a tailwind.config.js; las clases son tw-p-4 y hover:tw-p-2.

Si añades Tailwind a un proyecto con Bootstrap o con un CSS antiguo, las clases flex, hidden o p-4 pueden chocar. Un prefijo evita la colisión:

@import "tailwindcss" prefix(tw);
<div class="tw:p-4 tw:hover:bg-red-500">Con prefijo</div>
<div class="p-4">Sin efecto: no es una clase de Tailwind</div>
:root, :host {
  --tw-color-red-500: oklch(63.7% 0.237 25.331);
  --tw-spacing: 0.25rem;
}
.tw\:p-4 {
  padding: calc(var(--tw-spacing) * 4);
}
@media (hover: hover) {
  .tw\:hover\:bg-red-500:hover {
    background-color: var(--tw-color-red-500);
  }
}

Tres observaciones comprobadas:

  • El prefijo va siempre primero: tw:hover:p-2, nunca hover:tw:p-2.
  • Las variables de salida también llevan prefijo: --tw-color-red-500. Si las lees en tu CSS, debes escribirlo.
  • En @theme sigues declarando --color-marca, sin prefijo. La salida es --tw-color-marca.

Para añadir Tailwind a un sitio existente, lo habitual es combinar prefijo y ausencia de Preflight:

@layer theme, base, components, utilities;

@import "tailwindcss/theme.css" layer(theme) prefix(tw);
@import "tailwindcss/utilities.css" layer(utilities) prefix(tw);
Objetivo Tailwind 4.3 Tailwind 3.4
Quitar Preflight Importar por piezas corePlugins: { preflight: false }
Prefijo prefix(tw), clases tw:p-4 prefix: 'tw-', clases tw-p-4
Todo importante @import "tailwindcss" important important: true

15. Caso práctico: el catálogo de la tienda

Juntamos todo el módulo en una página real de la tienda de EMPRESA. Usa Preflight, una marca propia en @theme, estilos de etiquetas en @layer base, modificadores y escala de espaciado.

Catálogo con tres tarjetas de producto: UltraBook, Workstation con aviso de pocas unidades, y Básico agotado

Figura 11. El catálogo terminado, con 960 px de ancho.

Crea app-tienda.css:

@import "tailwindcss";

@theme {
  --color-marca-100: oklch(94% 0.03 275);
  --color-marca-600: oklch(50% 0.18 275);
  --color-marca-700: oklch(42% 0.16 275);
  --color-agotado: oklch(55% 0.2 25);
}

@layer base {
  h1 {
    font-size: var(--text-2xl);
    font-weight: var(--font-weight-bold);
    color: var(--color-gray-900);
  }

  h2 {
    font-size: var(--text-lg);
    font-weight: var(--font-weight-semibold);
    color: var(--color-gray-900);
  }

  a {
    color: var(--color-marca-600);
    text-decoration: underline;
    text-underline-offset: 2px;
  }

  button:not(:disabled) {
    cursor: pointer;
  }
}

Y tienda.html:

<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Tienda EMPRESA</title>
  <link rel="stylesheet" href="/app-tienda.css">
</head>
<body class="bg-gray-100 p-8 text-gray-700">
  <main class="mx-auto max-w-4xl">
    <h1>Portátiles y sobremesas</h1>
    <p class="mt-1 text-sm">Precios con IVA. Consulta las <a href="#">condiciones de envío</a>.</p>

    <ul class="mt-6 grid grid-cols-3 gap-5">
      <li>
        <article class="flex h-full flex-col rounded-xl border border-gray-200 bg-white p-5">
          <p class="text-xs font-semibold tracking-wider text-marca-600 uppercase">Portátiles</p>
          <h2 class="mt-1">Portátil UltraBook 14</h2>
          <p class="mt-2 text-sm/6">Portátil ligero de 14 pulgadas, 16 GB RAM</p>
          <p class="mt-auto pt-4 text-2xl font-bold text-gray-900">899 €</p>
          <button class="mt-3 rounded-lg bg-marca-600 px-4 py-2 text-sm font-medium text-white hover:bg-marca-700">Añadir a la cesta</button>
        </article>
      </li>
      <li>
        <article class="flex h-full flex-col rounded-xl border border-gray-200 bg-white p-5">
          <p class="text-xs font-semibold tracking-wider text-marca-600 uppercase">Sobremesas</p>
          <h2 class="mt-1">Torre Workstation</h2>
          <p class="mt-2 text-sm/6">Estación de trabajo para diseño y CAD</p>
          <p class="mt-auto pt-4 text-2xl font-bold text-gray-900">1899 €</p>
          <p class="mt-1 w-fit rounded-full bg-amber-500/15 px-2.5 py-0.5 text-xs font-medium text-amber-800">Quedan 4</p>
          <button class="mt-3 rounded-lg bg-marca-600 px-4 py-2 text-sm font-medium text-white hover:bg-marca-700">Añadir a la cesta</button>
        </article>
      </li>
      <li>
        <article class="flex h-full flex-col rounded-xl border border-gray-200 bg-white p-5">
          <p class="text-xs font-semibold tracking-wider text-marca-600 uppercase">Portátiles</p>
          <h2 class="mt-1">Portátil Básico 15</h2>
          <p class="mt-2 text-sm/6">Portátil de 15 pulgadas para ofimática</p>
          <p class="mt-auto pt-4 text-2xl font-bold text-gray-400 line-through">499 €</p>
          <p class="mt-1 w-fit rounded-full bg-agotado/10 px-2.5 py-0.5 text-xs font-medium text-agotado">Agotado</p>
          <button disabled class="mt-3 rounded-lg bg-marca-600 px-4 py-2 text-sm font-medium text-white opacity-50">No disponible</button>
        </article>
      </li>
    </ul>
  </main>
</body>
</html>

Qué aplica de lo visto

  • Preflight: el <ul> no lleva viñetas, por lo que sirve como contenedor semántico de la rejilla. Los <button> no traen aspecto propio.
  • @layer base: h1, h2 y a tienen estilo sin una sola clase. Medido en el navegador: h1 de 24 px y peso 700, h2 de 18 px y peso 600. Están en una capa para que puedas cambiarlos con text-3xl en una página concreta.
  • Tema: --color-marca-600 crea bg-marca-600 y text-marca-600. Si cambias de marca, solo tocas cuatro líneas.
  • Modificadores: bg-amber-500/15 da el fondo suave de la etiqueta y text-sm/6 fija la altura de línea.
  • Escala: px-2.5 py-0.5 usa múltiplos de 0,25.
  • Sin conflictos: el botón agotado no sobrescribe nada. Simplemente lleva otras clases.

mt-auto empuja el precio hacia abajo dentro del flex, para que todos los botones se alineen. Lo estudiarás en Flexbox.

💡 Consejo: Estas tres tarjetas repiten muchas clases. En un proyecto real serían un bucle o un componente, nunca tres copias. Se ve en Reutilización y componentes.

Equivalente en v3

Compilado en 3.4.19 con el mismo HTML. La configuración sustituye a @theme, y las reglas base usan theme():

// tailwind.config.js
export default {
  content: ['./*.html'],
  theme: {
    extend: {
      colors: {
        marca: { 100: '#e0e2f6', 600: '#4a50c7', 700: '#3a3fa3' },
        agotado: '#c8102e',
      },
    },
  },
};
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  h1 {
    font-size: theme(fontSize.2xl);
    font-weight: theme(fontWeight.bold);
    color: theme(colors.gray.900);
  }

  a {
    color: theme(colors.marca.600);
    text-decoration: underline;
  }
}

⚠️ Importante: En v3 los colores van en hexadecimal por un motivo. Con un valor oklch(...) en la configuración, la clase bg-marca/10 no genera nada, porque v3 solo sabe aplicar opacidad a los formatos que puede descomponer. Con hexadecimal, bg-agotado/10 produce rgb(200 16 46 / 0.1). Además, el border no necesita color: en v3 ya es gris.


16. Depurar una clase que no funciona

Cuando una clase no hace lo que esperas, sigue estos pasos en orden:

  1. Mira el elemento en las herramientas de desarrollo. ¿La clase está en el atributo class, y escrita como crees?
  2. Busca la regla en el panel de estilos.
    • Si no aparece, Tailwind no la generó: pasos 3 y 4.
    • Si aparece tachada, otra regla gana: paso 5.
  3. Comprueba que existe. Prueba a escribirla en HTML con la extensión de VS Code: si no se autocompleta, sospecha.
  4. Comprueba que el escáner la ve. ¿Está escrita entera? ¿Está en un fichero que Tailwind recorre? ¿La versión es la que crees?
  5. Mira quién gana. Busca CSS sin capa, un style o una clase rival. El panel “Computado” (Computed) te lleva a la regla ganadora.

Errores frecuentes

Síntoma Causa Solución
La clase no está en el CSS Nombre construido por partes: `bg-${c}-500` Escribir las clases enteras
mt-17 no hace nada Estás en v3 mt-[4.25rem]
mt-1.3 no hace nada No es múltiplo de 0,25 mt-[0.325rem] o un valor de la escala
bg-[--marca] no da color Sintaxis de v3 en v4 bg-(--marca)
flex! no hace nada Sintaxis de v4 en v3 !flex
text-[var(--x)] no cambia el tamaño Se interpreta como color text-(length:--x)
La clase está en el CSS y no gana CSS sin capa Moverlo a @layer components o base
Dos clases se pisan al azar Conflicto de la misma propiedad Dejar una, o tailwind-merge
Un <h1 class="text-4xl"> no crece Regla h1 sin capa Pasarla a @layer base
Un título no se ve grande sin clases Preflight funcionando Añadir clases o una regla en @layer base
La lista perdió las viñetas Preflight list-disc list-inside
border no dibuja nada en v3 Preflight desactivado border-solid
Las cajas miden más de lo previsto Preflight desactivado: content-box box-border
<input> invisible Preflight quita el borde border y un color
bg-foo no funciona La variable está en :root Declararla en @theme
Variable del tema vacía en JavaScript Tailwind la quitó por no usarse Usarla en el código o @theme static
w-1/2 falla en querySelector La barra debe escaparse CSS.escape('w-1/2')

💡 Consejo: Una prueba rápida para saber si es problema de generación o de cascada: añade ! a la clase. Si sigue sin funcionar, no se ha generado o no coincide con el elemento. Si funciona, hay una regla que gana.


17. Ejercicios

Ejercicio 1: Disección

Identifica variante, utilidad, valor, modificador e importante en cada clase, e indica en qué versiones funciona:

  • lg:focus:text-rose-700/80!
  • -mt-4
  • text-sm/6
  • dark:bg-(--marca)
  • [&>li]:py-1
Ver solución
Clase Variantes Utilidad y valor Modificador Importante v3.4 v4.3
lg:focus:text-rose-700/80! lg:, focus: text- con rose-700 /80 Sí No: habría que escribir lg:focus:!text-rose-700/80 Sí
-mt-4   mt- con 4 negativo     Sí Sí
text-sm/6   text- con sm /6, altura de línea   Sí Sí
dark:bg-(--marca) dark: bg- con variable     No: dark:bg-[--marca] Sí
[&>li]:py-1 [&>li]:, variante arbitraria py- con 1     Sí Sí

El punto clave es que la barra de text-sm/6 no es opacidad: en un tamaño de texto, el modificador es la altura de línea.

Ejercicio 2: De CSS a utilidades

Convierte esta regla en clases de Tailwind. Usa la escala y la paleta.

.aviso {
  padding: 0.75rem 1rem;
  border-radius: 0.5rem;
  border-left: 4px solid; /* color ámbar 500 */
  background-color: /* ámbar 50 */;
  color: /* ámbar 900 */;
  font-size: 0.875rem;
  font-weight: 500;
}
Ver solución
<p class="rounded-lg border-l-4 border-amber-500 bg-amber-50 px-4 py-3 text-sm font-medium text-amber-900">
  Quedan pocas unidades
</p>

Valores medidos en el navegador: padding-left: 16px, padding-top: 12px, border-left-width: 4px, font-size: 14px, font-weight: 500 y border-radius: 8px.

No hace falta border-solid, porque Preflight ya deja el estilo en solid. text-sm también fija la altura de línea, cosa que la regla original no hacía, así que el aviso puede variar un poco en altura.

Ejercicio 3: ¿Qué genera el escáner?

Tienes este fichero src/etiquetas.js. Di qué clases estarán en el CSS final, y corrígelo para que funcione con cualquier estado.

const estados = ['disponible', 'agotado'];

function claseFondo(estado) {
  const color = estado === 'agotado' ? 'rose' : 'emerald';
  return `bg-${color}-500`;
}

// Recuerda: el diseño de la etiqueta es text-white y rounded-full
Ver solución

Se generan text-white y rounded-full, porque el comentario se lee como texto. No se generan bg-rose-500 ni bg-emerald-500: esos nombres no aparecen en ningún sitio. Las palabras rose y emerald no son clases.

const fondos = {
  disponible: 'bg-emerald-500',
  agotado: 'bg-rose-500',
};

function claseFondo(estado) {
  return fondos[estado];
}

Y añade text-white rounded-full en el HTML o en el código, no lo dejes en un comentario: funcionaría por casualidad y dejaría de hacerlo si alguien borra la nota.

Ejercicio 4: Títulos con estilo propio

En una página de la tienda, los h1 y h2 deben verse grandes por defecto, pero en la cabecera un h1 concreto debe ser más pequeño con text-lg. Escribe el CSS y justifica dónde lo pones.

Ver solución
@import "tailwindcss";

@layer base {
  h1 {
    font-size: var(--text-2xl);
    font-weight: var(--font-weight-bold);
    color: var(--color-gray-900);
  }

  h2 {
    font-size: var(--text-lg);
    font-weight: var(--font-weight-semibold);
    color: var(--color-gray-900);
  }
}
<h1 class="text-lg">Cabecera con título discreto</h1>

Va en @layer base porque son estilos de etiquetas. Al estar en esa capa, text-lg de utilities lo sobrescribe. Sin la capa, el h1 sin capa ganaría, y text-lg no tendría efecto.

Medido sin la clase: h1 de 24 px y peso 700, h2 de 18 px y peso 600.

Ejercicio 5: Vista compacta

Crea una clase .densa que reduzca a 0.2rem la escala de un bloque sin cambiar ninguna utilidad de sus hijos. ¿Cuánto medirá un p-4 dentro?

Ver solución
@import "tailwindcss";

.densa {
  --spacing: 0.2rem;
}
<section class="densa">
  <div class="p-4">Tarjeta</div>
</section>

p-4 es calc(var(--spacing) * 4), que aquí vale 0,8 rem. Medido en el navegador: 12,8 px.

Esta regla está sin capa a propósito: solo cambia una variable y no compite con ninguna utilidad, porque ninguna clase define --spacing en el elemento. En v3 no se puede hacer así, porque p-4 lleva 1rem escrito en el CSS.

Ejercicio 6: Limpieza de valores arbitrarios

Este HTML se ha escrito sin mirar la documentación, y una línea ni siquiera funciona. Mejóralo en Tailwind 4.3.

<div class="h-[3.75rem] bg-[#4f46e5] text-[0.875rem]">a</div>
<div style="--ancho: 12rem" class="w-[var(--ancho)] bg-[--color-teal-600]">b</div>
<div class="grid grid-cols-[2fr_1fr]">c</div>
<div class="[transform:skewY(-3deg)]">d</div>
Ver solución
<div class="h-15 bg-indigo-600 text-sm">a</div>
<div style="--ancho: 12rem" class="w-(--ancho) bg-teal-600">b</div>
<div class="grid grid-cols-[2fr_1fr]">c</div>
<div class="-skew-y-3">d</div>
Antes Después Motivo
h-[3.75rem] h-15 15 × 0,25 rem. Mismo valor: 60 px
bg-[#4f46e5] bg-indigo-600 Paleta del tema. Cuidado: el tono es ligeramente distinto, porque ese hexadecimal es el índigo de v3
text-[0.875rem] text-sm Mismo tamaño, y además fija la altura de línea
w-[var(--ancho)] w-(--ancho) Forma corta: 192 px
bg-[--color-teal-600] bg-teal-600 Esta era la línea rota: en v4 genera CSS no válido
grid-cols-[2fr_1fr] Se queda igual No hay utilidad para esas columnas, y este uso es correcto
[transform:skewY(-3deg)] -skew-y-3 Existe utilidad, y además se combina con otras transformaciones

Ejercicio 7: El botón que no cambia de color

Un compañero tiene esto y dice que Tailwind no funciona, porque el botón sale índigo:

@import "tailwindcss";

.boton-marca {
  background-color: var(--color-indigo-600);
  color: white;
}
<button class="boton-marca bg-emerald-600">Pagar</button>

Explica la causa y arréglalo sin usar !.

Ver solución

.boton-marca está fuera de toda capa, y el CSS sin capa gana a @layer utilities, sea cual sea la especificidad. Al moverla a components, la utilidad gana:

@import "tailwindcss";

@layer components {
  .boton-marca {
    background-color: var(--color-indigo-600);
    color: white;
  }
}

Medido en Chromium: sin capa, oklch(0.511 0.262 276.966) (índigo); con capa, oklch(0.596 0.145 163.225) (esmeralda).

En v3 el mismo fichero habría funcionado si la regla se escribía antes de @tailwind utilities, y por eso este error aparece tanto al migrar.

Ejercicio 8: Un botón con modificadores

Escribe un botón con fondo índigo 600 al 90 %, texto blanco, tamaño base con altura de línea de 24 px, y relleno de 20 px en horizontal y 8 px en vertical. Usa el menor número de clases posible.

Ver solución
<button class="rounded-lg bg-indigo-600/90 px-5 py-2 text-base/6 font-semibold text-white">
  Comprar
</button>
  • bg-indigo-600/90: solo el fondo es translúcido, de modo que el texto sigue siendo blanco puro. Con opacity-90 también se aclararía el texto.
  • text-base/6: 16 px de tamaño y 6 × 4 = 24 px de altura de línea en una sola clase.
  • px-5: 5 × 4 = 20 px. py-2: 8 px.

Medido: line-height: 24px, font-size: 16px, y el fondo con opacidad 0,9.

Ejercicio 9: Predice el ganador

Sin ejecutar nada, di qué valor final tiene cada caso en Tailwind 4.3, y si cambiaría en 3.4.

  1. block hidden
  2. px-2 p-8
  3. mt-2 mt-10
  4. text-left text-center
  5. text-sm text-lg
  6. text-lg leading-8
  7. rounded-lg rounded-t-none
Ver solución

Salida real de ambas versiones, en el orden de la hoja:

4.3.3   .mt-2 .mt-10 .block .flex .hidden .rounded-lg .rounded-t-none ...
        .text-center .text-left .text-lg .text-sm .leading-8
3.4.19  .mt-10 .mt-2 .block .flex .hidden .rounded-lg .rounded-t-none ...
        .text-left .text-center .text-lg .text-sm .leading-8
Caso Resultado en 4.3 ¿Cambia en 3.4?
1 hidden: va después de block No
2 32 px arriba y abajo, 8 px a los lados: px-2 va después de p-8 No
3 mt-10: orden numérico Sí, gana mt-2
4 text-left Sí, gana text-center
5 text-sm No
6 Tamaño de text-lg, altura de leading-8 No
7 Solo las esquinas inferiores redondeadas No

Los casos 2, 6 y 7 son combinaciones correctas: lo particular ajusta lo general. Los casos 1, 3, 4 y 5 son conflictos que no deberían estar en tu código. Que tengan respuesta no significa que sea buena idea depender de ella.

Ejercicio 10 (propuesto): Una tarjeta de empleado

Con los datos de la API de EMPRESA, escribe una tarjeta de empleado con nombre, puesto y departamento, sin una sola línea de CSS propio salvo un color de marca en @theme. Usa un modificador de opacidad en el fondo de la insignia de departamento.

Ejercicio 11 (propuesto): Tailwind dentro de Bootstrap

Crea una página con Bootstrap por CDN y añade Tailwind con prefijo tw y sin Preflight. Comprueba que el .btn de Bootstrap se conserva. Prueba luego a activar Preflight y observa qué se rompe.

Ejercicio 12 (propuesto): El catálogo en v3

Compila la tienda de la sección 15 con Tailwind 3.4. Comprueba qué cambia en la figura: el color del borde, si quitas border-gray-200, y el cursor de los botones, ya sin la regla de @layer base.


Resumen

Concepto API Idea clave
Utility-first p-4, text-sm, flex Componer clases pequeñas en el HTML, con valores de una escala
Anatomía md:hover:bg-red-500/50! Variantes, utilidad, valor, modificador e importante
Generación Escáner y compilador Lee texto plano y solo crea lo que encuentra
Clases dinámicas `bg-${c}-500` No funciona: la clase debe aparecer entera
Detección 🔄 v4.0 @source / content Automática en v4; lista manual en v3
Capas 🔄 v4.0 @layer Reales en v4: la especificidad no cuenta entre capas
CSS sin capa Regla fuera de @layer Vence a todas las capas: escribe siempre dentro de una
Preflight @import "tailwindcss" Quita los estilos del navegador: cada aspecto se da con clases
Extender Preflight @layer base Estilos de etiquetas, siempre en la capa
Quitar Preflight 🔄 v4.0 Importar por piezas / corePlugins Sin él, las cajas vuelven a content-box
Valores por defecto 🔄 v4.0 border, placeholder, cursor currentColor, 50 % y default en v4
Tema 🔄 v4.0 @theme / tailwind.config.js Las variables crean utilidades
Variables usadas @theme static Solo se emiten las usadas salvo con static
Funciones --spacing(), --alpha(), --theme() Leer el tema desde CSS; en v3, theme()
Escala 🔄 v4.0 --spacing: 0.25rem p-4 es calc(var(--spacing) * 4); cambiable en un subárbol
Valores dinámicos 🆕 v4.0 mt-17, grid-cols-15 Las utilidades calculan; múltiplos de 0,25
Valores arbitrarios w-[327px], [&>li]:p-2 Salida de emergencia; _ es un espacio
Variables en clases 🔄 v4.0 bg-(--marca) bg-[--marca] es un falso amigo: no da error ni color
Modificadores /50, /7 Ajustan el valor; no son variantes
Importante 🔄 v4.0 flex!, important Al final en v4; al principio en v3
Orden Hoja de estilos Decide la hoja, no el atributo class; evita los conflictos
Conflictos 🔄 v4.0 mt-2 mt-10 Cambian de ganador de v3 a v4: deja solo una clase
Prettier prettier-plugin-tailwindcss Ordena como el compilador
Prefijo 🔄 v4.0 prefix(tw) tw:p-4 en v4; tw-p-4 en v3