Saltar al contenido

Tailwind y el enfoque utility-first

Integrar Tailwind v4 en Astro 7 mediante el plugin de Vite, contrastar la filosofía utility-first con los estilos scoped, hacerlos convivir en un mismo componente con el detalle de @reference y las variables de tema, e importar otros frameworks CSS decidiendo con criterio cuándo conviene cada modelo.

Tailwind en Astro 7: el plugin de Vite

En Astro 7 la integración @astrojs/tailwind es historia: Tailwind v4 se instala como plugin de Vite, la vía moderna y más directa. El comando astro add tailwind hace el trabajo por ti —instala tailwindcss y @tailwindcss/vite, registra el plugin en la clave vite de la configuración y crea un global.css con la única línea que Tailwind necesita para arrancar—.

JS
// astro.config.mjsimport { defineConfig } from 'astro/config';import tailwindcss from '@tailwindcss/vite';export default defineConfig({  vite: { plugins: [tailwindcss()] },});

La configuración de Tailwind v4 es CSS-first: ya no hay un tailwind.config.js obligatorio. Declaras tu tema en el propio CSS con la directiva @theme, y esos valores se publican como variables CSS que todo el proyecto puede leer. Basta con importar ese global.css desde tu layout raíz para que las utilidades queden disponibles en cada página.

CSS
/* src/styles/global.css */@import "tailwindcss";@theme {  --color-marca: #7c3aed;  --radius-tarjeta: 0.75rem;}

Ese global.css no se activa solo: has de importarlo una vez desde tu layout raíz, como cualquier hoja global, y a partir de ahí las utilidades quedan disponibles en todas las páginas que el layout envuelve.

ASTRO
---// Layout.astroimport '../styles/global.css';---<html lang="es">  <body><slot /></body></html>

Dos filosofías: utility-first y scoped

Antes de mezclarlos, hay que entender qué defiende cada modelo, porque no compiten por el mismo terreno. La utilidad atómica y la regla scoped resuelven problemas distintos, y confundirlos lleva a usar mal ambos.

ASTRO
<!-- utility-first: la apariencia vive en el marcado --><button class="inline-flex items-center gap-2 rounded-lg bg-violet-600 px-4 py-2 text-white hover:bg-violet-700">  Comprar</button>

La utilidad gana en velocidad y en disciplina: como los valores salen de una escala, es difícil producir el desajuste de tres grises casi iguales que plaga los proyectos con CSS libre. El scoped gana en expresividad y en semántica: cuando una animación o un estado complejo convertiría el atributo class en un muro ilegible de utilidades, una regla con nombre propio es más clara y más mantenible.

Ninguno de los dos escala bien fuera de su terreno. Reproducir un sistema de espaciado consistente a base de reglas scoped repetidas es tedioso y propenso a la deriva; expresar una animación de varios fotogramas o un estado con cinco variantes como una ristra de utilidades en el atributo class es un jeroglífico. Cada modelo castiga precisamente aquello para lo que el otro está pensado, y esa complementariedad es la que justifica tenerlos ambos a mano en lugar de forzar uno para todo.

Cómo conviven: el detalle de @reference

La combinación es el escenario real, y esconde un matiz que conviene dominar. Astro procesa cada bloque <style> de forma aislada, así que un bloque scoped no conoce, por defecto, el tema ni las utilidades de Tailwind. Si quieres usar @apply o las funciones de tema dentro de un <style> de componente, has de declarar el contexto con @reference, que le indica a Tailwind dónde vive tu configuración sin duplicar el CSS generado.

ASTRO
<style>  @reference "../styles/global.css";  .cta { @apply inline-flex items-center gap-2 rounded-lg; }</style>

Ahora bien, muchas veces no necesitas @apply en absoluto. Como Tailwind v4 publica el tema en forma de variables CSS globales, dentro de un bloque scoped puedes leer var(--color-marca) directamente, sin @reference y sin acoplar el componente a las utilidades. Esa es, casi siempre, la vía más limpia: las utilidades en el marcado, y las variables de tema como puente hacia el CSS scoped cuando de verdad hace falta escribir una regla.

En la práctica, esa vía se ve así de sencilla, sin @reference de por medio:

ASTRO
<style>  /* lee el token publicado por @theme, sin acoplarse a las utilidades */  .cta {    background: var(--color-marca);    border-radius: var(--radius-tarjeta);  }</style>

El componente consume el tema de Tailwind como consumiría cualquier variable CSS, y queda desacoplado del motor de utilidades: si mañana cambiaras de framework, esta regla seguiría siendo CSS válido. Es la frontera más limpia posible entre ambos mundos, y la que conviene preferir por defecto.

Otros frameworks y el criterio final

Tailwind no es la única opción, y Astro las acepta casi todas por la misma puerta: la importación global. Un conjunto de tokens como Open Props, un framework clásico como Bootstrap o Bulma, o una hoja mínima como Pico entran importando su CSS desde el layout, exactamente igual que tu global.css. Y si prefieres otro motor de utilidades, UnoCSS ofrece su propio plugin de Vite y encaja en el mismo hueco que Tailwind.

Importar cualquiera de ellos es el gesto que ya conoces: una o dos líneas en el frontmatter del layout, y su CSS entra en el pipeline junto al tuyo.

ASTRO
---// Layout.astroimport 'open-props/style';import 'open-props/normalize';---

La lección transversal es que Astro no privilegia ningún framework de estilos: todos entran por la importación global o por un plugin de Vite, y todos conviven con el scoping sin fricción. Tu elección deja de ser una restricción impuesta por el framework y pasa a ser, simplemente, una decisión de diseño que puedes revisar cuando quieras.

El criterio para elegir, más allá de la moda, se reduce a la naturaleza del estilo. Lo utilitario —espaciado, rejilla, color, tipografía de una escala— pide utility-first: rápido, consistente y sin nombres. Lo idiosincrásico —una animación, un estado con muchas variantes, un componente con personalidad visual propia— pide scoped: semántico, contenido y expresivo. No es una guerra de bandos, sino un reparto de tareas, y Astro está construido para que ese reparto sea fluido en lugar de forzado.