wandres.dev
ESTILOS · scoped, global, CSS

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.

⏱ 16 min

Hasta aquí, el estilo ha vivido en el bloque <style> del componente: CSS a medida, semántico y acotado. El enfoque utility-first invierte ese modelo. En lugar de escribir reglas, compones la apariencia directamente en el marcado con clases atómicas y globales tomadas de una escala de diseño. Astro no toma partido: abraza los dos modelos y te deja combinarlos. La destreza está en saber cuándo usar cada uno y cómo hacerlos convivir sin fricción.

🎯 Al terminar esta lección sabrás
  • Instalar Tailwind v4 en Astro 7 con el plugin oficial de Vite.
  • Contrastar la filosofía utility-first con la de los estilos scoped.
  • Combinar utilidades y <style> scoped en un mismo componente usando @reference y variables de tema.
  • Importar otros frameworks CSS y decidir con criterio entre scoped y utility-first.

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—.

// astro.config.mjs
import { 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.

/* 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.

---
// Layout.astro
import '../styles/global.css';
---
<html lang="es">
  <body><slot /></body>
</html>
📝
Adiós a la integración, hola al plugin

Si vienes de proyectos antiguos, el cambio mental es este: donde antes registrabas una integración de Astro en el array integrations, ahora registras un plugin de Vite en la clave vite. Es coherente con la dirección de Astro 7, que apoya cada vez más el pipeline en Vite y su bundler Rolldown. El astro add tailwind te oculta el detalle, pero conviene saber en qué capa vive ahora Tailwind.

ℹ️
Tailwind v4 detecta tus clases solo

En la versión 4 desaparece la lista content que antes había que mantener a mano: Tailwind rastrea automáticamente los ficheros del proyecto y genera solo las utilidades que de verdad usas. Menos configuración que mantener y menos ocasiones de que una clase deje de aplicarse por haberse quedado fuera de la lista.

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.

Utility-first (Tailwind)

Compones en el marcado con clases de una escala fija. Rapidísimo para maquetar, sin nombres que inventar y con consistencia impuesta por el sistema.

🎯

Scoped a medida

Escribes CSS semántico y local. Brilla en lo complejo: estados, pseudoelementos, @keyframes, lógica de selectores que como utilidades se volvería ilegible.

🤝

Los dos juntos

Utilidades para el ochenta por ciento —espaciado, layout, color— y un <style> scoped para el veinte a medida. Comparten tema por variables.

<!-- 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.

<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:

<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.

flowchart TD
GLOBAL[global.css con import tailwindcss y theme] --> UTIL[utilidades en el marcado]
GLOBAL --> TOK[tokens como variables CSS]
TOK --> SCOPED[bloque style scoped a medida]
UTIL --> UI[interfaz final]
SCOPED --> UI
style GLOBAL fill:#89b4fa,color:#11111b
style UI fill:#a6e3a1,color:#11111b
💡
Prefiere la variable de tema al @apply

@apply es cómodo, pero recrea en tu CSS lo que la utilidad ya expresa, y obliga a arrastrar @reference por cada bloque. Cuando solo necesitas un valor —un color, un radio, un espaciado—, léelo como variable de tema con var(...). Reserva @apply para cuando de verdad quieras empaquetar un grupo de utilidades bajo un nombre semántico reutilizable, no como reflejo automático.

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.

---
// Layout.astro
import '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.

Utility-first y scoped no son rivales: son dos ejes de un mismo plano

La discusión eterna entre utility-first y CSS semántico suele plantearse como una elección de bando, y ahí está su error de raíz. Son respuestas a preguntas distintas. Las utilidades atómicas optimizan la composición: convierten el diseño en un vocabulario finito de decisiones ya tomadas, de modo que maquetar es elegir de un menú en vez de inventar valores, y la consistencia deja de depender de la disciplina para volverse una propiedad del sistema. El CSS scoped optimiza la expresión: te devuelve todo el poder del lenguaje —la cascada, los estados, las animaciones, los selectores complejos— dentro de una frontera segura donde nada se derrama. Un proyecto maduro no elige entre ambos; los sitúa en ejes perpendiculares y se mueve por el plano según el problema. La mayor parte de la interfaz vive en el eje de la composición, donde las utilidades brillan; los focos de complejidad visual viven en el eje de la expresión, donde el scoped es insustituible; y las variables de tema son la charnela que mantiene ambos ejes hablando el mismo idioma de colores y medidas. Astro es, quizá, el primer framework que trata esta convivencia como el caso normal y no como una excepción a gestionar: no te pide que declares una filosofía de estilos al empezar, sino que te da un lienzo donde el CSS global, las utilidades y el scope coexisten sin fricción. Dominar los estilos en Astro no es, por tanto, elegir la herramienta correcta de una vez y para siempre, sino aprender a leer cada fragmento de interfaz y saber en qué eje del plano resolverlo.

⚔️ Mide los dos modelos en tu proyecto
  1. Ejecuta astro add tailwind, revisa qué añadió a astro.config.mjs y confirma que existe un global.css con @import "tailwindcss" importado desde tu layout.
  2. Declara un token con @theme y consúmelo primero como utilidad en el marcado y luego como var(--...) dentro de un <style> scoped.
  3. Escribe un componente con estado y animación en scoped, y su versión solo con utilidades; compara cuál se lee mejor.
  4. Añade @reference a un bloque scoped para usar @apply, y después reescribe esa regla usando la variable de tema; decide cuál prefieres y por qué.