Tailwind 4: las utilidades no se aplican (CSS sin @layer)

Migras a Tailwind CSS 4 (o empiezas un proyecto nuevo, por ejemplo con shadcn/ui en Next.js) y algunas utilidades dejan de tener efecto. La clase está en el HTML y la regla aparece en el CSS compilado, pero el estilo calculado sigue siendo el global:

<div class="border border-red-500">Aviso</div>
<h1 class="text-white">Título</h1>
getComputedStyle(document.querySelector(".border-red-500")).borderTopColor
// el gris de tu regla global, no el rojo de la utilidad

Pasa con border-*, divide-*, text-* y cualquier utilidad que choque con una regla global de tu hoja de estilos. No hay mensaje de error: en las herramientas de desarrollo del navegador, la declaración de la utilidad suele aparecer tachada. Este artículo explica la causa y cómo corregirla.

Por qué pasa

Tailwind 4 usa capas de cascada nativas de CSS. El archivo que importa con @import "tailwindcss" declara el orden theme, base, components, utilities, inyecta Preflight en la capa base y coloca todas las utilidades en la capa utilities.

La regla de CSS que rige aquí está documentada en MDN: los estilos que no están dentro de ninguna capa siempre prevalecen sobre los estilos declarados en capas, para declaraciones normales (sin !important). Además, una vez establecido el orden de capas, la especificidad y el orden de aparición dejan de contar entre capas distintas. Por eso, una regla global como esta gana a cualquier utilidad, aunque la utilidad tenga una clase y la regla global solo un selector * o h1:

* {
  border-color: hsl(var(--border));
}

h1, h2, h3 {
  color: var(--text-heading);
}

En Tailwind 3 esto no ocurría con estos selectores. Allí no se usaban capas nativas (la directiva @layer la procesaba Tailwind), y la clase de la utilidad ganaba por especificidad al selector * o h1. En Tailwind 4 la comparación se decide antes, por capas, y el CSS sin capa gana. Si pegas el mismo código en un proyecto nuevo, falla sin avisar.

Solución

  1. Localiza las reglas sin capa. Abre tu hoja de estilos global (en Next.js suele ser app/globals.css) y busca selectores que estén fuera de cualquier @layer: *, html, body, h1, a, etc. Las que fijan propiedades que las utilidades también controlan (color, borde, fuente, fondo) son las sospechosas.

  2. Muévelas a @layer base. Es la capa que Tailwind reserva para los estilos por defecto de elementos y la que usa su documentación para ese fin:

    @import "tailwindcss";
    
    @layer base {
      * {
        border-color: hsl(var(--border));
      }
    
      h1, h2, h3 {
        color: var(--text-heading);
      }
    }

    La guía de instalación manual de shadcn/ui sigue el mismo patrón: sus reglas de * y body están dentro de @layer base. Las variables CSS de :root y .dark no compiten con las utilidades, así que no hace falta moverlas; la guía de migración de shadcn/ui a Tailwind 4 incluso las saca de @layer base.

  3. No confundas acotar con dar prioridad. Si una regla no debe afectar a todo el documento, limita su alcance (por ejemplo .contenido h1), pero déjala igualmente dentro de una capa. Acotar el selector no devuelve la prioridad a las utilidades: una regla sin capa gana sin importar lo específico que sea su selector. Si quieres que sea un estilo de componente que las utilidades puedan sobrescribir, usa @layer components.

  4. Pon en una capa el CSS de terceros. Una hoja de estilos importada sin capa tiene el mismo efecto. Impórtala dentro de una capa:

    @import "tailwindcss";
    @import "libreria-ui/estilos.css" layer(components);
  5. Revisa las pantallas después del cambio. Elementos que se veían grises por error ahora mostrarán el color real de su utilidad. Un caso típico: un spinner con border-white sobre un fondo claro, que antes se veía gris y ahora queda invisible. Recorre las pantallas en modo claro y oscuro.

Cómo verificar que quedó resuelto

Mide el estilo calculado antes y después del cambio, en modo claro y en modo oscuro (alterna el modo como lo haga tu proyecto, por ejemplo con la clase dark en <html>). En la consola del navegador:

for (const sel of [".border-red-500", ".border-info\\/30", "h1.text-white"]) {
  const el = document.querySelector(sel);
  if (el) {
    const estilo = getComputedStyle(el);
    console.log(sel, estilo.borderTopColor, estilo.color);
  }
}

Antes de la corrección, todos los bordes devuelven el mismo gris global y el título conserva su color global. Después, cada elemento devuelve el color de su utilidad. Para descartar regresiones, ejecuta también el lint, la comprobación de tipos y la compilación de producción de tu proyecto.

Si comparas dos versiones del CSS compilado desde un script de Node con @tailwindcss/postcss, ejecuta cada una en un proceso distinto. El plugin guarda en caché el compilador por archivo de entrada y lo reutiliza entre compilaciones, así que dos compilaciones en el mismo proceso pueden no ser independientes.

Variantes y errores relacionados

Los bordes se ven del color del texto tras migrar

No es este problema. En Tailwind 4, el color de borde por defecto de border-* y divide-* pasó a ser currentColor (en Tailwind 3 era gray-200). La guía de actualización ofrece una regla de compatibilidad que, de nuevo, va dentro de @layer base:

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

Una clase propia en @layer utilities deja de funcionar con variantes

En Tailwind 4, @layer utilities ya no registra utilidades propias: la guía de actualización indica usar @utility para que funcionen con variantes como hover o lg.

!important como parche

MDN explica que !important invierte el orden: las declaraciones importantes dentro de una capa ganan a las importantes fuera de ella. Aun así, forzar cada utilidad es un parche frágil. Es mejor corregir la capa de la regla global.

Resumen

  • Si una utilidad de Tailwind 4 está en el CSS compilado pero no se aplica, busca reglas globales fuera de @layer.
  • El CSS sin capa gana al CSS en capas, sin importar la especificidad; las utilidades de Tailwind 4 están en @layer utilities.
  • Mueve esas reglas a @layer base (o a components) e importa el CSS de terceros con layer(...).
  • Revisa las pantallas: los elementos que parecían grises ahora muestran su color real, y algunos (como un borde blanco) pueden dejar de verse.
  • Verifica con getComputedStyle antes y después, en modo claro y oscuro.

Referencias

Comentarios