Módulo 18: Tailwind en React, Angular y otros
Objetivos del módulo
- Entender qué cambia al usar Tailwind con un framework (la entrada de la hoja de estilos) y qué no cambia (el motor)
- Instalar Tailwind en React con Vite, en Angular con PostCSS, en Next.js, en Vue y en Svelte
- Usar Tailwind con webpack mediante
@tailwindcss/webpackopostcss-loader - Dar el equivalente exacto de cada instalación en Tailwind 3.4
- Reconocer por qué los restos de CSS de una plantilla derrotan a tus utilidades, y arreglarlo con capas
- Saber por qué cada CSS Module y cada bloque de estilos de componente necesita
@reference - Evitar el error de usar
@import "tailwindcss"dentro de un componente - Explicar por qué
bg-${color}-500falla, incluso cuando parece funcionar - Resolver las clases dinámicas con objetos de clases completas, variables CSS o
@source inline - Usar las sintaxis propias de cada framework:
[class.x],[ngClass],:classyclass:x - Configurar Prettier para ordenar clases en plantillas y funciones de cada framework
- Diagnosticar los fallos habituales con una tabla de síntomas
1. Qué cambia y qué no cambia con un framework
Hasta ahora hemos escrito HTML puro. Este es el único módulo del curso que usa componentes, así que conviene empezar por una idea tranquilizadora: Tailwind no sabe qué es un componente. Solo hace tres cosas, y ninguna depende del framework:
- Lee tus ficheros como texto y busca palabras que parezcan clases.
- Genera el CSS de las que existen.
- Entrega una hoja de estilos normal.
Por eso, en el navegador no queda ni rastro de Tailwind: ni JavaScript, ni estado, ni dependencias de ejecución. Lo que aprendiste en los módulos anteriores sirve tal cual. flex, hover: o dark: funcionan igual en un .tsx, un .vue o una plantilla de Angular.
Lo único que varía es por dónde entra la hoja de estilos en la compilación. Cada herramienta de construcción tiene su mecanismo, y Tailwind ofrece un paquete para cada una:
Figura 1. El framework decide la puerta de entrada. El motor y el resultado son los mismos.
| Tu proyecto | Paquete | Dónde se configura |
|---|---|---|
| React, Vue o Svelte con Vite | @tailwindcss/vite | vite.config.ts |
| Angular | @tailwindcss/postcss | .postcssrc.json |
| Next.js | @tailwindcss/postcss | postcss.config.mjs |
| Proyecto con webpack | @tailwindcss/webpack | webpack.config.mjs |
| Sin empaquetador | @tailwindcss/cli | Línea de órdenes |
La instalación con Vite, PostCSS y la CLI ya la viste en el módulo 01, con HTML puro. Aquí nos centramos en lo que cada framework añade, y sobre todo en lo que hace tropezar: los componentes traen sus propias hojas de estilos, y eso obliga a entender dos cosas nuevas (@reference y las capas frente al CSS del componente).
El hilo del módulo es sencillo: la misma tarjeta de producto de la tienda EMPRESA, escrita en React, Angular, Vue, Svelte y Next.js. Tiene una etiqueta de existencias cuyo color depende del dato, que es justo el caso en el que aparecen las clases dinámicas. Verás que las clases apenas cambian entre versiones: solo cambia cómo cada framework las aplica.
Todas las capturas de este módulo salen de compilaciones reales con estas versiones:
| Paquete | Versión |
|---|---|
| tailwindcss, @tailwindcss/vite, @tailwindcss/postcss, @tailwindcss/webpack | 4.3.3 |
| react, react-dom | 19.3.1 |
| @angular/core | 22.2.4 |
| next | 16.4.3 |
| vue, @vitejs/plugin-vue | 3.5.44, 6.0.10 |
| svelte, @sveltejs/vite-plugin-svelte | 5.57.3, 7.3.1 |
| vite | 8.3.5 |
| webpack, css-loader, mini-css-extract-plugin | 5.112.2, 7.1.5, 2.10.2 |
📘 Concepto: No necesitas saber React, Angular, Vue ni Svelte para seguir este módulo. Basta con leer el apartado del framework que uses y las secciones comunes (capas,
@referencey clases dinámicas). Si quieres aprender a fondo alguno de ellos, tienes los cursos de React y Angular.
2. React con Vite
Instalación 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no existe
@tailwindcss/viteni@import "tailwindcss". Si usas la LTS: instalatailwindcss@3.4.19 postcss autoprefixer, creapostcss.config.jsytailwind.config.jsconcontent, y escribe las tres directivas@tailwindensrc/index.css. El código completo está en la sección 9.
Crea el proyecto e instala Tailwind:
npm create vite@latest mi-tienda -- --template react-ts
cd mi-tienda
npm install tailwindcss @tailwindcss/vite
⚠️ Importante: En PowerShell, el
--de esa orden se pierde y obtienes la plantilla vanilla sin avisar. Escríbelo entre comillas:npm create vite@latest mi-tienda "--" --template react-ts. En Git Bash funciona tal cual.
Añade el plugin a vite.config.ts:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});
Y sustituye todo el contenido de src/index.css. Añadimos ya un color de marca, que usaremos a lo largo del módulo:
@import "tailwindcss";
@theme {
--color-marca: #4338ca;
}
src/main.tsx ya importa ese fichero, de modo que no hace falta tocarlo. Es la forma de entrar en el grafo de módulos de Vite: el plugin detecta el @import y compila esa hoja. Borra también src/App.css y su import en App.tsx; en la sección 3 verás por qué no conviene dejarlos.
La tarjeta en React
En JSX, el atributo es className, porque class es una palabra reservada de JavaScript. Por lo demás, son las mismas clases de los módulos anteriores:
import { EtiquetaStock } from './EtiquetaStock';
type Producto = { nombre: string; descripcion: string; precio: number; stock: number };
export function TarjetaProducto({ producto }: { producto: Producto }) {
return (
<article className="flex flex-col gap-3 rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
<div className="flex items-start justify-between gap-2">
<h3 className="font-semibold text-slate-900">{producto.nombre}</h3>
<EtiquetaStock stock={producto.stock} />
</div>
<p className="text-sm text-slate-600">{producto.descripcion}</p>
<p className="mt-auto text-xl font-bold text-marca">{producto.precio} €</p>
<button
disabled={producto.stock === 0}
className="rounded-lg bg-marca px-4 py-2 text-sm font-medium text-white hover:bg-indigo-800 disabled:bg-slate-300"
>
Añadir al carrito
</button>
</article>
);
}
La etiqueta elige su color según las existencias. Fíjate en que el objeto contiene clases completas; la sección 6 explica por qué esto no es un detalle de estilo:
const estilos = {
alto: 'bg-emerald-100 text-emerald-800',
bajo: 'bg-amber-100 text-amber-800',
agotado: 'bg-rose-100 text-rose-800',
};
export function EtiquetaStock({ stock }: { stock: number }) {
const nivel = stock === 0 ? 'agotado' : stock < 5 ? 'bajo' : 'alto';
return (
<span className={`shrink-0 rounded-full px-2 py-0.5 text-xs font-medium ${estilos[nivel]}`}>
{nivel}
</span>
);
}
Y el componente principal recorre los datos. Son tres productos reales de la API de EMPRESA, con las existencias retocadas para ver los tres niveles:
import { productos } from './productos';
import { TarjetaProducto } from './TarjetaProducto';
export function App() {
return (
<main id="catalogo" className="mx-auto max-w-4xl bg-slate-50 p-6">
<h2 className="mb-4 text-2xl font-bold text-slate-900">Catálogo de EMPRESA</h2>
<div className="grid gap-4 sm:grid-cols-3">
{productos.map((p) => (
<TarjetaProducto key={p.id} producto={p} />
))}
</div>
</main>
);
}

Figura 2. El catálogo en React. Más adelante verás esta misma tarjeta en otros cuatro frameworks.
💡 Consejo: Cuando un componente reciba clases desde fuera por
className, usaclsxytailwind-merge. Lo explicamos en el módulo 16, y allí encontrarás la funcióncny el sistema de botones conclass-variance-authority. No es específico de React:clsxfunciona con cualquier framework.
Server Components y CSS en tiempo de compilación
Tailwind genera el CSS al compilar, no en el navegador. Es una diferencia importante frente a las bibliotecas de CSS-in-JS que calculan estilos al renderizar. Como no necesita JavaScript en ejecución, encaja sin esfuerzo en los Server Components, que verás en la sección 8.
3. La primera trampa: el CSS que dejó la plantilla
Este es, con diferencia, el fallo más frecuente al añadir Tailwind a un proyecto existente. Las plantillas de los frameworks traen CSS propio. La de Vite con React incluye reglas como estas en src/index.css:
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
body {
margin: 0;
}
Imagina que no las borras, o que el proyecto ya tenía estilos propios. Reproducimos el problema con unas reglas simplificadas:
@import "tailwindcss";
/* Restos de la plantilla, fuera de cualquier capa */
h1 {
font-size: 56px;
line-height: 1.1;
}
a {
color: #646cff;
}
.boton {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
color: white;
background-color: #7c3aed;
}
<h1 className="text-sm text-slate-600">Título con text-sm</h1>
<button className="boton bg-rose-600">Comprar</button>
<a href="#" className="text-rose-600">Enlace con text-rose-600</a>

Figura 3. Las tres utilidades se generan, y las tres pierden.
La causa está en cómo el navegador ordena las capas, que viste en el módulo 02. Tailwind 4 coloca sus utilidades dentro de @layer utilities, y el CSS que no está en ninguna capa gana siempre a todo el que sí lo está, sin importar la especificidad ni el orden. Un simple selector a derrota a .text-rose-600, aunque la clase sea más específica. Muchos alumnos pierden tiempo buscando un problema de especificidad o añadiendo !, cuando el motivo es otro.
La solución: mete tu CSS en una capa
No hace falta borrar ese CSS. Basta con colocarlo en la capa que le corresponde:
@import "tailwindcss";
@layer base {
h1 {
font-size: 56px;
line-height: 1.1;
}
a {
color: #646cff;
}
}
@layer components {
.boton {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
color: white;
background-color: #7c3aed;
}
}

Figura 4. Mismo HTML, mismas reglas. Dentro de una capa, las utilidades vuelven a mandar.
Los estilos de elementos van en base y las clases propias en components. Las dos capas se declaran antes que utilities, así que cualquier utilidad las sobrescribe.
La trampa también en Tailwind 3 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4)
@layerno genera capas nativas de CSS: Tailwind mueve las reglas de sitio y compiten por especificidad y orden. Si usas la LTS: tu CSS suelto ya no gana siempre, pero un selector más específico, o una regla colocada después de@tailwind utilities, sí vencerá a una utilidad. Coloca tus reglas antes de esa directiva o en@layer components.
⚠️ Importante: Esta regla vale para cualquier CSS que no esté en una capa: CSS Modules,
<style scoped>de Vue,<style>de Svelte y estilos de componentes de Angular. Lo comprobarás con Angular en la sección 7. Un componente con una regla propia sobrebackground-colorno se dejará cambiar porbg-*desde la plantilla.
4. CSS Modules y @reference
Hay ocasiones en las que quieres una hoja propia para un componente: migras poco a poco, o necesitas una animación compleja, o un selector difícil de expresar con clases. Veamos un aviso con un CSS Module. Como primer intento, lo escribimos igual que en el módulo 16:
/* Aviso.module.css */
.aviso {
@apply rounded-lg border border-amber-300 bg-amber-50 p-4 text-sm text-amber-900;
}
.titulo {
@apply font-semibold text-marca;
}
Al compilar, falla:
Error: Cannot apply unknown utility class `rounded-lg`. Are you using CSS modules or similar and missing `@reference`?
Por qué: cada hoja se compila por separado
Tu index.css y Aviso.module.css son dos ficheros CSS distintos, y la herramienta de construcción los procesa de forma independiente. Cuando Tailwind recibe Aviso.module.css, no sabe nada del otro fichero: no ha cargado el tema, así que ni siquiera conoce rounded-lg.
La tentación es copiar @import "tailwindcss" en el módulo. No lo hagas: funciona, pero duplica Preflight y el tema en cada componente. La solución correcta es @reference, que carga el tema solo para consultarlo, sin emitir nada de él:
@reference 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no existe
@referenceni hace falta:tailwind.config.jses global, y@applyfunciona en cualquier hoja procesada por PostCSS. Si usas la LTS: escribe el CSS Module directamente, sin esa línea, y cambia el bloque@variant hoverpor@apply hover:border-amber-500;.
/* Aviso.module.css */
@reference "./index.css";
.aviso {
@apply rounded-lg border border-amber-300 bg-amber-50 p-4 text-sm text-amber-900;
@variant hover {
@apply border-amber-500;
}
}
.titulo {
@apply font-semibold text-marca;
}
import estilos from './Aviso.module.css';
export function Aviso() {
return (
<div className={estilos.aviso}>
<p className={estilos.titulo}>Envío gratuito</p>
<p>Desde 50 € en toda la península.</p>
</div>
);
}

Figura 5. El CSS Module usa el color de marca definido en index.css.
Figura 6. @reference da acceso al tema sin volver a generar Preflight ni utilidades.
La ruta de @reference es relativa al fichero que la contiene. Hay dos formas válidas:
| Línea | Qué ve | Cuándo usarla |
|---|---|---|
@reference "./index.css"; | Tu tema completo | Si tienes @theme propio (lo habitual) |
@reference "tailwindcss"; | Solo el tema por defecto | Si no personalizas nada |
Si usas la segunda y tu módulo contiene una clase del tema propio, obtienes un error más corto, sin ninguna pista sobre @reference:
Error: Cannot apply unknown utility class `text-marca`
Qué genera, y una sorpresa útil
Este es el CSS real del título, ya minimizado:
._titulo_15e1w_2 {
--tw-font-weight: var(--font-weight-semibold, 600);
font-weight: var(--font-weight-semibold, 600);
color: var(--color-marca, #4338ca);
}
Fíjate en el segundo argumento de var(). Bajo @reference, Tailwind no emite las variables del tema, y no puede asegurar que existan en el navegador. Por eso añade el valor como reserva. En una utilidad normal verías solo var(--color-marca).
Esto no es un detalle sin importancia: si cambias --color-marca en la hoja principal, el componente lo recoge igual en ejecución, y además sigue funcionando aunque esa variable no llegue a emitirse.
Variables del tema en un CSS escrito a mano 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no existen
--theme()ni las variables del tema. Si usas la LTS: lee el valor con la funcióntheme()y notación de puntos, por ejemplocolor: theme('colors.exclusiva');.
También puedes leer el tema sin @apply. Existen dos caminos:
@reference "./index.css";
.exclusiva {
color: var(--color-exclusiva); /* Sin valor de reserva */
}
.exclusivaBien {
color: --theme(--color-exclusiva); /* var(--color-exclusiva, #be123c) */
}
Con la versión 4.3.3 y @tailwindcss/vite, las dos han funcionado: una variable usada solo en el CSS Module llega igualmente al :root de la hoja principal. Una variable del tema que no se usa en ningún sitio, en cambio, no se emite. --theme() aporta el valor de reserva, y además falla al compilar si te equivocas de nombre, cosa que var() nunca hará:
Error: Could not resolve value for theme function: `theme(--color-exclusiva)`.
No importes Tailwind dentro de un componente
Medimos qué ocurre al cambiar @reference por @import "tailwindcss" en un solo CSS Module:
| Hoja del módulo | CSS total de la página | Preflight |
|---|---|---|
@reference | Unos 15 kB | 1 vez |
@import "tailwindcss" | Unos 21 kB | 2 veces |
Compila sin errores, y por eso pasa desapercibido. Con un módulo son 6 kB; con cincuenta componentes se nota. En Angular el resultado es peor todavía, como verás en la sección 7.
💡 Consejo: Antes de recurrir a un CSS Module, pregúntate si no bastan las utilidades en el marcado. Lo explicamos en la sección sobre
@applycon mesura.@referencees la herramienta para cuando de verdad necesitas una hoja aparte.
5. Vue y Svelte
Vue y Svelte comparten dos cosas: usan Vite y tienen componentes de un solo fichero, con un bloque de estilos propio. Ese bloque se compila aparte, de modo que necesita @reference.
Vue 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no se usa
@tailwindcss/viteni@reference. Si usas la LTS: usapostcss.config.js,tailwind.config.jsconcontent: ['./index.html', './src/**/*.{ts,vue}']y las directivas@tailwind;@applyfunciona en<style scoped>sin ninguna línea extra.
npm create vite@latest mi-tienda -- --template vue-ts
cd mi-tienda
npm install tailwindcss @tailwindcss/vite
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [vue(), tailwindcss()],
});
La hoja de entrada se llama src/style.css en la plantilla (nosotros usamos estilos.css). Sustituye su contenido por el mismo @import "tailwindcss" y el mismo @theme, por las razones de la sección 3.
En Vue, :class admite un objeto. La clave es la clase, y el valor decide si se aplica. Se combina con el class estático:
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
producto: { nombre: string; descripcion: string; precio: number; stock: number };
}>();
const nivel = computed(() =>
props.producto.stock === 0 ? 'agotado' : props.producto.stock < 5 ? 'bajo' : 'alto',
);
</script>
<template>
<article class="flex flex-col gap-3 rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
<div class="flex items-start justify-between gap-2">
<h3 class="font-semibold text-slate-900">{{ producto.nombre }}</h3>
<span
class="shrink-0 rounded-full px-2 py-0.5 text-xs font-medium"
:class="{
'bg-emerald-100 text-emerald-800': nivel === 'alto',
'bg-amber-100 text-amber-800': nivel === 'bajo',
'bg-rose-100 text-rose-800': nivel === 'agotado',
}"
>
{{ nivel }}
</span>
</div>
<p class="text-sm text-slate-600">{{ producto.descripcion }}</p>
<p class="mt-auto text-xl font-bold text-marca">{{ producto.precio }} €</p>
<button class="boton" :disabled="producto.stock === 0">Añadir al carrito</button>
</article>
</template>
<style scoped>
@reference "./estilos.css";
.boton {
@apply rounded-lg bg-marca px-4 py-2 text-sm font-medium text-white hover:bg-indigo-800 disabled:bg-slate-300;
}
</style>

Figura 7. La misma tarjeta en Vue. El botón sale de un <style scoped> con @reference.
scoped añade un atributo al selector para aislarlo. El resultado real es .boton[data-v-ea43df70]. Sin la línea @reference, el error es el mismo que viste con los CSS Modules, y una regla scoped sin capa también derrota a las utilidades (sección 3).
📘 Concepto: Las claves entrecomilladas de
:classson cadenas completas, escritas en el fichero, y por eso Tailwind las encuentra. En la sección 6 verás que esa es la condición que importa, y no la sintaxis del framework.
Svelte 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no se usa
@tailwindcss/viteni@reference. Si usas la LTS: usapostcss.config.jsytailwind.config.jsconcontent: ['./index.html', './src/**/*.{ts,svelte}']. Lo hemos probado:@applyfunciona en<style>sin preprocesador y sinlang="postcss".
npm create vite@latest mi-tienda -- --template svelte-ts
cd mi-tienda
npm install tailwindcss @tailwindcss/vite
// vite.config.ts
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [svelte(), tailwindcss()],
});
Aquí la hoja de la plantilla es src/app.css. Svelte tiene una directiva propia para las clases condicionales, class:nombre={condición}, y Tailwind la reconoce, incluido el formato con guiones:
<script lang="ts">
let { producto }: {
producto: { nombre: string; descripcion: string; precio: number; stock: number };
} = $props();
const nivel = $derived(producto.stock === 0 ? 'agotado' : producto.stock < 5 ? 'bajo' : 'alto');
</script>
<article class="flex flex-col gap-3 rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
<div class="flex items-start justify-between gap-2">
<h3 class="font-semibold text-slate-900">{producto.nombre}</h3>
<span
class="shrink-0 rounded-full px-2 py-0.5 text-xs font-medium"
class:bg-emerald-100={nivel === 'alto'}
class:text-emerald-800={nivel === 'alto'}
class:bg-amber-100={nivel === 'bajo'}
class:text-amber-800={nivel === 'bajo'}
class:bg-rose-100={nivel === 'agotado'}
class:text-rose-800={nivel === 'agotado'}
>
{nivel}
</span>
</div>
<p class="text-sm text-slate-600">{producto.descripcion}</p>
<p class="mt-auto text-xl font-bold text-marca">{producto.precio} €</p>
<button class="boton" disabled={producto.stock === 0}>Añadir al carrito</button>
</article>
<style>
@reference "./estilos.css";
.boton {
@apply rounded-lg bg-marca px-4 py-2 text-sm font-medium text-white hover:bg-indigo-800 disabled:bg-slate-300;
}
</style>

Figura 8. La misma tarjeta en Svelte. Las etiquetas usan la directiva class:.
La directiva class: escribe el nombre de la clase en la plantilla, y por eso se detecta. Svelte también aísla el <style> con una clase propia: el selector real es .boton.svelte-p3j2w8. Sin @reference, la compilación falla con el mismo mensaje de siempre.
6. Clases dinámicas: por qué bg-${color}-500 falla
Si solo te quedas con una sección de este módulo, que sea esta. Es el error que más veces se repite en proyectos reales con cualquier framework.
El error
Queremos colorear el estado de un pedido. Tenemos cuatro estados, y parece natural guardar un color por estado y construir la clase:
const colores = {
ENTREGADO: 'emerald',
ENVIADO: 'sky',
PENDIENTE: 'amber',
CANCELADO: 'rose',
} as const;
type Estado = keyof typeof colores;
export function EstadoMal({ estado }: { estado: Estado }) {
const color = colores[estado];
return (
<span className={`rounded-full px-3 py-1 text-xs font-medium bg-${color}-100 text-${color}-800`}>
{estado}
</span>
);
}
El código es correcto: en el navegador, el atributo class contiene bg-sky-100 text-sky-800. Compila sin errores ni avisos. Y sin embargo:

Figura 9. Tres de los cuatro estados no tienen color. El que sí lo tiene funciona por casualidad.
Mira la figura con atención, porque enseña algo peor que un fallo. ENTREGADO sí sale verde. Debajo hay otro componente que escribe bg-emerald-100 entero, de modo que esa clase existe en el CSS, y nuestra plantilla, por pura suerte, la aprovecha.
Por eso este error es tan traicionero: el día que alguien borre el otro componente, la etiqueta perderá su color sin que nadie haya tocado su código. Y si pruebas solo con el estado que funciona, no lo verás.
La causa
Tailwind no ejecuta tu código. Su escáner lee cada fichero como texto plano y busca palabras completas con forma de clase. No sabe qué es una plantilla de cadena, ni una variable, ni un componente:
Figura 10. La cadena bg-${color}-100 no existe como palabra hasta que el código se ejecuta, y para entonces Tailwind ya ha terminado.
La regla es corta: toda clase debe aparecer escrita entera en algún fichero que Tailwind lea. No importa dónde: en un objeto, en un array, en una función. Importa que el texto completo esté ahí.
Solución 1: un objeto con clases completas
Es la mejor opción para un conjunto cerrado de valores, que es el caso habitual:
const estilos = {
ENTREGADO: 'bg-emerald-100 text-emerald-800',
ENVIADO: 'bg-sky-100 text-sky-800',
PENDIENTE: 'bg-amber-100 text-amber-800',
CANCELADO: 'bg-rose-100 text-rose-800',
} as const;
export type Estado = keyof typeof estilos;
export function EstadoPedido({ estado }: { estado: Estado }) {
return (
<span className={`rounded-full px-3 py-1 text-xs font-medium ${estilos[estado]}`}>
{estado}
</span>
);
}

Figura 11. Con clases completas, los cuatro estados tienen color.
Además del escáner, ganas otras cosas: TypeScript exige que todo estado tenga su entrada, y puedes buscar bg-sky-100 en el proyecto y encontrarla. La plantilla de cadena de esta solución no plantea ningún problema, porque solo une clases que ya están escritas enteras.
Solución 2: una variable CSS para valores continuos 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no existe la sintaxis con paréntesis. Si usas la LTS: usa corchetes, con
w-[--avance]ybg-[--tono], que generanvar(--avance)yvar(--tono). No lleves esa forma a v4, dondebg-[--marca]compila a CSS no válido.
Un objeto no sirve cuando el valor es un número cualquiera o un color que llega de la base de datos. No puedes escribir cien clases w-[1%], w-[2%]… Para eso, pasa el dato por una variable CSS y usa una clase fija que la lea:
import type { CSSProperties } from 'react';
export function BarraProgreso({ valor, color }: { valor: number; color: string }) {
const variables = { '--avance': `${valor}%`, '--tono': color } as CSSProperties;
return (
<div className="h-3 w-full overflow-hidden rounded-full bg-slate-200" style={variables}>
<div className="h-full w-(--avance) rounded-full bg-(--tono)" />
</div>
);
}
<BarraProgreso valor={72} color="#0d9488" />
<BarraProgreso valor={35} color="#ea580c" />
.w-\(--avance\) { width: var(--avance); }
.bg-\(--tono\) { background-color: var(--tono); }

Figura 12. Las clases son siempre las mismas. Lo que cambia es el valor de la variable.
La clase w-(--avance) aparece entera en el fichero, así que se genera una sola vez, y sirve para cualquier valor. Comparada con poner style={{ width }} directamente, tiene una ventaja: conserva las variantes, de modo que puedes escribir hover:bg-(--tono) o md:w-(--avance), algo imposible con estilos en línea.
Solución 3: forzar clases con @source inline 🆕 v4.1
🆕 Desde Tailwind 4.1. No existe en la LTS (Tailwind 3.4). Si usas la LTS: añade
safelistatailwind.config.js, por ejemplosafelist: [{ pattern: /^bg-violet-200$/ }]. En Tailwind 4.0 no hay equivalente directo: escribe las clases en un fichero que Tailwind lea.
A veces la clase ya viene hecha desde fuera: un CMS, una base de datos o una API devuelven el texto bg-violet-200. No está en ningún fichero tuyo, y por tanto no se genera. Simulamos ese caso:
// Simula una clase que llega en tiempo de ejecución: no está escrita entera en ningún fichero.
const claseDeLaApi = ['bg', 'violet', '200'].join('-');
export function DesdeApi() {
return <span className={`rounded px-3 py-1 text-sm ${claseDeLaApi}`}>Clase llegada de la API</span>;
}
@source inline declara clases que deben existir aunque nadie las escriba. Admite la expansión con llaves que viste en el módulo 01:
@import "tailwindcss";
@source inline("bg-violet-200");
/* O una familia completa: bg-sky-100, text-sky-100, bg-sky-800... */
@source inline("{bg,text}-{sky,violet}-{100,800}");
⚠️ Importante: Cada clase forzada pesa en el CSS final, se use o no. Si la lista de valores es abierta (cualquier color que escriba un cliente), no sirve de nada: usa la solución 2.
@source inlinetiene sentido cuando conoces de antemano un catálogo pequeño de clases que llegan desde fuera.
Una trampa añadida: dist sin .gitignore
Al preparar este ejemplo nos llevamos una sorpresa: bg-violet-200 sí aparecía en el CSS sin haberla declarado. El minimizador había reducido ['bg', 'violet', '200'].join('-') a la cadena 'bg-violet-200' en el JavaScript de dist/. Como el proyecto de prueba no tenía .gitignore, Tailwind leía esa carpeta en la compilación siguiente y generaba la clase. Con dist borrado y en .gitignore, la clase dejó de aparecer.
Es otra forma de “funciona por casualidad”: la clase aparece a partir de la segunda compilación, y falta en el servidor de integración continua, donde dist no existe. Tailwind respeta .gitignore, como viste en el módulo 01, así que mantén ahí tus carpetas de salida. Las plantillas de Vite ya incluyen dist.
¿Qué solución elijo?
| Situación | Solución |
|---|---|
| Conjunto cerrado de valores conocidos (estados, tamaños, variantes) | Objeto con clases completas |
| Valor continuo o arbitrario (porcentaje, color de usuario) | Variable CSS y bg-(--var) |
| Catálogo pequeño de clases que llegan ya hechas desde fuera | @source inline |
| Combinación de muchas variantes de un componente | class-variance-authority o tailwind-variants (módulo 16) |
7. Angular con PostCSS
Angular no usa el plugin de Vite: su sistema de construcción acepta plugins de PostCSS, así que el paquete que corresponde es @tailwindcss/postcss.
Instalación con el asistente de Angular 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no hay
.postcssrc.jsonni@tailwindcss/postcss. Si usas la LTS: instalatailwindcss@3.4.19, creatailwind.config.jsen la raíz concontent: ['./src/**/*.{html,ts}']y pon las directivas@tailwindensrc/styles.css. Angular detecta ese fichero solo, sin configurar PostCSS. El asistente--style=tailwindinstala la versión 4, así que no te sirve.
Angular CLI puede hacerlo todo por ti, al crear el proyecto o después:
# Proyecto nuevo
ng new mi-tienda --style=tailwind
# Proyecto existente
ng add tailwindcss
El asistente instala tailwindcss, @tailwindcss/postcss y postcss, crea .postcssrc.json y añade @import 'tailwindcss'; a la hoja global. Si lo haces a mano, son estos dos ficheros, en este orden:
npm install tailwindcss @tailwindcss/postcss postcss
{
"plugins": {
"@tailwindcss/postcss": {}
}
}
/* src/styles.css */
@import "tailwindcss";
@theme {
--color-marca: #4338ca;
}
⚠️ Importante: Angular solo lee la configuración de PostCSS desde
postcss.config.jsono.postcssrc.json, en formato JSON. Unpostcss.config.jso.mjscomo el de Next.js no le sirve.
La tarjeta en Angular
Con plantillas en fichero aparte, el marcado es casi igual al de Vue:
<!-- tarjeta-producto.html -->
<article class="flex flex-col gap-3 rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
<div class="flex items-start justify-between gap-2">
<h3 class="font-semibold text-slate-900">{{ producto().nombre }}</h3>
<app-etiqueta-stock [stock]="producto().stock" />
</div>
<p class="text-sm text-slate-600">{{ producto().descripcion }}</p>
<p class="mt-auto text-xl font-bold text-marca">{{ producto().precio }} €</p>
<button class="boton" [disabled]="producto().stock === 0">Añadir al carrito</button>
</article>
// tarjeta-producto.ts
import { Component, input } from '@angular/core';
import { EtiquetaStock } from './etiqueta-stock';
type Producto = { nombre: string; descripcion: string; precio: number; stock: number };
@Component({
selector: 'app-tarjeta-producto',
imports: [EtiquetaStock],
templateUrl: './tarjeta-producto.html',
styleUrl: './tarjeta-producto.css',
host: { class: 'grid' },
})
export class TarjetaProducto {
readonly producto = input.required<Producto>();
}
// etiqueta-stock.ts
import { Component, computed, input } from '@angular/core';
const estilos = {
alto: 'bg-emerald-100 text-emerald-800',
bajo: 'bg-amber-100 text-amber-800',
agotado: 'bg-rose-100 text-rose-800',
};
@Component({
selector: 'app-etiqueta-stock',
template: `
<span class="shrink-0 rounded-full px-2 py-0.5 text-xs font-medium" [class]="clases()">
{{ nivel() }}
</span>
`,
})
export class EtiquetaStock {
readonly stock = input.required<number>();
protected readonly nivel = computed(() => {
const unidades = this.stock();
return unidades === 0 ? 'agotado' : unidades < 5 ? 'bajo' : 'alto';
});
protected readonly clases = computed(() => estilos[this.nivel()]);
}

Figura 13. La misma tarjeta en Angular.
⚠️ Importante: Fíjate en
host: { class: 'grid' }. Es lo único que no existe en los otros frameworks. Angular deja en el DOM un elemento real,<app-tarjeta-producto>, que se interpone entre tu rejilla y el<article>. La rejilla estira ese elemento, pero el<article>que hay dentro conserva su altura. Con una descripción más larga en la primera tarjeta, medimos 266, 266 y 266 píxeles con la clase, y 266, 206 y 206 sin ella. El defecto no se ve si todos los textos ocupan las mismas líneas, de modo que puede tardar en aparecer. Probamos también conflex, y las tarjetas se encogían al contenido cuando faltaba ancho. Congrid, el hijo ocupa todo el elemento anfitrión.
Todas las formas de aplicar clases
Angular tiene varias sintaxis. Comprobamos que el escáner las detecta todas, incluidos los casos con caracteres especiales:
import { Component, signal } from '@angular/core';
import { NgClass } from '@angular/common';
@Component({
selector: 'app-clases',
imports: [NgClass],
template: `
<div id="clases" class="w-96 space-y-2 p-2">
<p [class.bg-sky-100]="activo()" [class.p-2]="activo()">[class.bg-sky-100]</p>
<p [class.hover:underline]="activo()" [class.w-1/2]="activo()" [class.bg-[#fde68a]]="activo()">
variante, fracción y arbitrario
</p>
<p [ngClass]="{ 'bg-lime-100 text-lime-800': activo() }">ngClass</p>
<p [class]="activo() ? 'bg-fuchsia-100' : 'bg-slate-100'">[class] con ternario</p>
</div>
`,
})
export class Clases {
protected readonly activo = signal(true);
}
| Sintaxis | Se detecta | Observaciones |
|---|---|---|
[class.bg-sky-100]="cond" | Sí | Una clase por enlace |
[class.hover:underline]="cond" | Sí | Admite variantes |
[class.w-1/2]="cond" | Sí | Admite fracciones |
[class.bg-[#fde68a]]="cond" | Sí | Admite valores arbitrarios |
[ngClass]="{ 'a b': cond }" | Sí | Requiere importar NgClass |
[class]="cond ? 'a' : 'b'" | Sí | Las dos cadenas están escritas enteras |
Vale la misma regla de la sección 6: [class]="'bg-' + color + '-100'" falla igual que en React.
Estilos de componente, @reference y presupuestos
Cada styleUrl o styles de Angular es una hoja independiente. Por eso funciona igual que un CSS Module y necesita @reference, con la ruta relativa al CSS del componente:
/* src/app/tarjeta-producto.css */
@reference "../styles.css";
.boton {
@apply rounded-lg bg-marca px-4 py-2 text-sm font-medium text-white hover:bg-indigo-800 disabled:bg-slate-300;
}
Sin esa línea, el error aparece ahora en la salida de Angular:
X [ERROR] Cannot apply unknown utility class `rounded-lg`. Are you using CSS modules or similar and missing `@reference`? [plugin angular-css]
La encapsulación de Angular convierte el selector en .boton[_ngcontent-%COMP%] en el JavaScript compilado. Y aquí sí que importar Tailwind en un componente tiene consecuencias inmediatas. Si cambias @reference por @import "tailwindcss", la compilación falla, porque cada componente tiene un presupuesto de tamaño:
▲ [WARNING] src/app/tarjeta-producto.css exceeded maximum budget. Budget 4.00 kB was not met by 6.57 kB with a total of 10.57 kB.
X [ERROR] src/app/tarjeta-producto.css exceeded maximum budget. Budget 8.00 kB was not met by 2.57 kB with a total of 10.57 kB.
Si te encuentras con este error, no subas el presupuesto de angular.json: cambia la línea por @reference.
El CSS del componente y las utilidades
La regla de la sección 3 también se cumple aquí, y es más fácil caer en ella, porque el CSS del componente se siente “privado”. Los dos botones siguientes llevan bg-rose-600:
template: `
<button class="boton-gris bg-rose-600">Sin capa</button>
<button class="fondo-capa bg-rose-600 text-white">Con capa</button>
`,
styles: `
.boton-gris {
background-color: #475569;
color: white;
padding: 0.25rem 0.75rem;
}
@layer components {
.fondo-capa {
background-color: #475569;
padding: 0.25rem 0.75rem;
}
}
`,

Figura 14. Las seis sintaxis funcionan. Abajo, el botón sin capa ignora bg-rose-600; el que está dentro de @layer components lo respeta.
Si un componente debe permitir que se cambie su aspecto desde fuera, coloca sus estilos en @layer components. ViewEncapsulation aísla los selectores, pero no cambia el orden de las capas.
💡 Consejo: Sass no está soportado en Tailwind 4 (lo viste en el módulo 01). Si tu proyecto de Angular se creó con
--style=scss, tienes que tomar una decisión antes de migrar: pasar a CSS, o quedarte con Tailwind 3.4.
8. Next.js
Instalación 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) el plugin de PostCSS es el propio paquete
tailwindcss. Si usas la LTS: instalatailwindcss@3.4.19 postcss autoprefixer, escribeplugins: { tailwindcss: {}, autoprefixer: {} }enpostcss.config.mjs, creatailwind.config.jsconcontent: ['./app/**/*.{ts,tsx}']y pon las directivas@tailwindenapp/globals.css.
create-next-app activa Tailwind por defecto (opción --tailwind). A mano son tres pasos:
npm install tailwindcss @tailwindcss/postcss postcss
// postcss.config.mjs
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};
export default config;
/* app/globals.css */
@import "tailwindcss";
@theme {
--color-marca: #4338ca;
}
Esa hoja se importa una sola vez, en el layout raíz. A partir de ahí, las clases funcionan en todo el árbol.
Server y Client Components
Para Tailwind no hay diferencia. Un Server Component produce HTML con clases, y un Client Component (marcado con 'use client') también. Las clases se detectan al compilar, leyendo el texto de los dos ficheros:
// app/page.tsx: Server Component, sin 'use client'
import { productos } from './productos';
import { TarjetaProducto } from './TarjetaProducto';
import { Contador } from './Contador';
export default function Inicio() {
return (
<main id="catalogo" className="mx-auto max-w-4xl p-6">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-2xl font-bold">Catálogo en Next.js</h2>
<Contador />
</div>
<div className="grid gap-4 sm:grid-cols-3">
{productos.map((p) => (
<TarjetaProducto key={p.id} producto={p} />
))}
</div>
</main>
);
}
// app/Contador.tsx: Client Component, se hidrata en el navegador
'use client';
import { useState } from 'react';
export function Contador() {
const [unidades, setUnidades] = useState(0);
return (
<button
onClick={() => setUnidades(unidades + 1)}
className="rounded-lg border border-slate-300 bg-white px-3 py-1 text-sm hover:bg-slate-100"
>
Carrito: {unidades}
</button>
);
}
TarjetaProducto y EtiquetaStock son los mismos ficheros de React, sin cambiar una línea.
Fuentes con next/font y @theme inline
next/font descarga la fuente al compilar, la sirve desde tu dominio y expone una variable CSS con el nombre que elijas. Para que font-sans la use, hay que pasar esa variable a Tailwind:
// app/layout.tsx
import type { ReactNode } from 'react';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
export const metadata = { title: 'Tienda EMPRESA' };
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="es" className={inter.variable}>
<body className="bg-slate-50 font-sans text-slate-900">{children}</body>
</html>
);
}
/* app/globals.css */
@import "tailwindcss";
@theme inline {
--font-sans: var(--font-inter);
}
@theme {
--color-marca: #4338ca;
}

Figura 15. La misma tarjeta en Next.js, ahora con Inter servida por next/font. Compara la tipografía con la de las otras capturas.
¿Por qué inline? Sin él, Tailwind define --font-sans en :root, y esa variable apunta a --font-inter. Aquí no habría problema, porque ponemos la clase en <html>, que es el propio :root. Pero muchos proyectos la ponen en <body> o en un contenedor, y entonces --font-inter no existe en :root y la referencia se pierde. Lo comprobamos con una variable definida en un <div>: sin inline, el <div> se quedó con la fuente heredada; con inline, usó la fuente indicada. La opción inline escribe var(--font-inter) directamente dentro de font-sans, y así la referencia se resuelve en el elemento que usa la clase. Lo estudiaste en el módulo 14.
@theme inline con next/font 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no existe
@theme. Si usas la LTS: entailwind.config.jsescribetheme: { extend: { fontFamily: { sans: ['var(--font-inter)'] } } }, que genera.font-sans { font-family: var(--font-inter) }.
Hojas por componente en Next.js
Los CSS Modules funcionan con Next.js igual que en la sección 4, con @reference al principio, y la ruta relativa apunta a globals.css. Lo comprobamos en Next.js 16.4.3, que compila con Turbopack por defecto. El nombre de la clase cambia (en la prueba fue .guE1ta_aviso), pero el contenido es el mismo, con los valores de reserva de la sección 4:
.guE1ta_aviso {
border-radius: var(--radius-lg, .5rem);
background-color: var(--color-amber-50, #fffbeb);
padding: calc(var(--spacing, .25rem) * 4);
color: var(--color-marca, #4338ca);
}
9. Equivalentes completos en Tailwind 3.4
Las cajas anteriores resumen lo que cambia. Aquí tienes los ficheros completos, compilados con tailwindcss@3.4.19 en React, Vue, Svelte, Next.js, webpack y Angular.
React, Vue y Svelte con Vite 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) Vite carga Tailwind a través de PostCSS, no con un plugin propio. Si usas la LTS: crea los tres ficheros de este apartado y quita
@tailwindcss/vitedevite.config.ts.
npm install -D tailwindcss@3.4.19 postcss autoprefixer
// postcss.config.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{ts,tsx}'],
theme: { extend: { colors: { marca: '#4338ca' } } },
plugins: [],
};
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Las extensiones de content cambian con el framework: {ts,tsx} en React, {ts,vue} en Vue y {ts,svelte} en Svelte. Si olvidas una, las clases de esos ficheros no se generan.
⚠️ Importante: Con
export default, el fichero necesita"type": "module"enpackage.json, que las plantillas de Vite ya traen. Sin ello, la compilación se detiene conSyntaxError: Unexpected token 'export'. Nos ocurrió en un proyecto creado connpm init -y, que escribe"type": "commonjs".

Figura 16. El catálogo compilado con Tailwind 3.4. Las clases de la tarjeta son las mismas. Las pequeñas diferencias de color y sombra vienen de la paleta y las escalas (módulo 05 y módulo 06).
En los estilos de componente desaparece @reference, porque la configuración es global:
/* Aviso.module.css en Tailwind 3.4 */
.aviso {
@apply rounded-lg border border-amber-300 bg-amber-50 p-4 text-sm text-amber-900;
}
.titulo {
@apply font-semibold text-marca;
}
Para las variantes dentro de @apply, usa @apply hover:border-amber-500;, que funciona en las dos versiones. @variant es solo de v4.
Angular 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) Angular activa Tailwind por sí solo si encuentra
tailwind.config.js. Si usas la LTS: no crees.postcssrc.json, porque si existe, Angular omite esa detección automática.
npm install -D tailwindcss@3.4.19
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{html,ts}'],
theme: { extend: { colors: { marca: '#4338ca' } } },
plugins: [],
};
/* src/styles.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@apply funciona directamente en el CSS de cada componente. La compilación de prueba generó .boton[_ngcontent-%COMP%] con los valores ya resueltos.
Next.js 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) se usa el plugin
tailwindcssen PostCSS. Si usas la LTS: usa estos tres ficheros yapp/globals.csscon las directivas@tailwind.
// postcss.config.mjs
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
// tailwind.config.js
module.exports = {
content: ['./app/**/*.{ts,tsx}'],
theme: {
extend: {
colors: { marca: '#4338ca' },
fontFamily: { sans: ['var(--font-inter)'] },
},
},
plugins: [],
};
10. Webpack
Vite se ha impuesto en los proyectos nuevos, pero sigue habiendo muchas aplicaciones con webpack.
@tailwindcss/webpack 🆕 v4.2
🆕 Desde Tailwind 4.2. No existe en la LTS (Tailwind 3.4). Si usas la LTS: usa
postcss-loaderconplugins: [require('tailwindcss'), require('autoprefixer')]enpostcss.config.js,contententailwind.config.jsy las directivas@tailwind. En Tailwind 4.0 y 4.1 usapostcss-loadercon@tailwindcss/postcss.
Antes de la versión 4.2 había que pasar por PostCSS. Ahora existe un loader propio, que se coloca el último de la lista, porque webpack aplica los loaders de derecha a izquierda, y Tailwind debe actuar antes que css-loader:
npm install tailwindcss @tailwindcss/webpack css-loader mini-css-extract-plugin
// webpack.config.mjs
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
export default {
mode: 'production',
entry: './src/index.js',
plugins: [new MiniCssExtractPlugin()],
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'],
},
],
},
};
// src/index.js
import './app.css';
document.querySelector('#salida').innerHTML =
'<p class="rounded-lg bg-emerald-100 p-4 text-emerald-800">Compilado con webpack</p>';
/* src/app.css */
@import "tailwindcss";
webpack 5.112.2 compiled successfully in 14050 ms
Fíjate en que el ejemplo escribe las clases dentro de una cadena de JavaScript, y aun así se detectan. Es la prueba de lo que decíamos: al escáner le da igual el contexto.
La alternativa con PostCSS sigue funcionando. Solo cambia el último loader y añade un fichero:
// En webpack.config.mjs
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
// postcss.config.js
module.exports = {
plugins: { '@tailwindcss/postcss': {} },
};
Comparamos los dos resultados byte a byte, y las dos configuraciones produjeron exactamente el mismo main.css, de 5015 bytes. Usa @tailwindcss/webpack en proyectos nuevos, porque te ahorras dependencias y un fichero de configuración. Si ya tienes postcss-loader configurado, no hay motivo urgente para cambiar.
11. Prettier en cada framework
Ordenar clases a mano es perder el tiempo. En el módulo 01 instalaste prettier-plugin-tailwindcss y viste sus opciones. Con frameworks hay dos novedades: reconoce sus atributos de plantilla, y conviene decirle qué funciones contienen clases:
{
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./app.css",
"tailwindFunctions": ["clsx", "cn"],
"singleQuote": true
}
Partimos de clases desordenadas en los tres formatos. En Angular y Vue, el plugin ordena también las cadenas de los atributos dinámicos, sin configurar nada:
<!-- Entrada -->
<button class="text-white px-4 bg-marca" [class]="'text-white px-4 bg-marca'" [ngClass]="{ 'text-white px-4 bg-marca': activo }">Comprar</button>
<!-- Salida en plantilla de Angular -->
<button
class="bg-marca px-4 text-white"
[class]="'bg-marca px-4 text-white'"
[ngClass]="{ 'bg-marca px-4 text-white': activo }"
>
Comprar
</button>
<!-- Salida en un componente de Vue -->
<template>
<button
class="bg-marca px-4 text-white"
:class="{ 'bg-marca px-4 text-white': activo }"
>
Comprar
</button>
</template>
En JSX se ordena className, y también los argumentos de las funciones declaradas. Las demás cadenas se quedan como están, como se ve en c:
const a = clsx(
'flex bg-marca px-4 text-white',
activo && 'flex-col p-2 hover:bg-indigo-800',
);
const b = cn('flex bg-marca px-4 text-white');
const c = 'text-white px-4 bg-marca flex'; // Sin tocar: no es un atributo ni una función declarada
return <button className="propia rounded-lg bg-marca px-4 text-white">...</button>;
Dos detalles de esa salida: bg-marca ocupa su lugar como cualquier otro color, porque tailwindStylesheet le da acceso a tu tema, y propia, que no es de Tailwind, pasa al principio. El objeto estilos de la sección 6 tampoco se ordena: lo probamos, y una cadena dentro de un objeto se queda como está. Tendrás que mantener ese orden a mano.
Configuración de Prettier 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) el plugin lee la configuración de JavaScript. Si usas la LTS: quita
tailwindStylesheety, si hace falta, añade"tailwindConfig": "./tailwind.config.js";tailwindFunctionsfunciona igual. Lo hemos probado con la versión 0.8.1 del plugin.
⚠️ Importante: Si combinas este plugin con otros (por ejemplo, el de Svelte),
prettier-plugin-tailwindcssdebe ser el último de la listaplugins. La documentación del plugin lo exige de forma expresa.
💡 Consejo: El autocompletado de VS Code se configura aparte: para que reconozca
cn()yclsx(), usa la misma lista de funciones en el ajustetailwindCSS.classFunctions, que viste en el módulo 01.
12. Tabla de diagnóstico
Cuando algo falla en un proyecto con framework, busca aquí el síntoma antes de añadir ! a ninguna clase:
| Síntoma | Causa probable | Solución |
|---|---|---|
Una clase no pinta nada, y en DevTools el class es correcto | La clase no se generó: está construida con una plantilla | Objeto con clases completas (sección 6) |
| Funciona en un componente y no en otro | Otro fichero contiene la clase por casualidad | La misma; escribe la clase entera en el componente |
| La clase existe en el CSS, pero aparece tachada en DevTools | CSS sin capa la está derrotando | Mete ese CSS en @layer (sección 3) |
Cannot apply unknown utility class y menciona @reference | Hoja de componente sin @reference | Añade @reference con la ruta a tu hoja principal |
Cannot apply unknown utility class con una clase de tu tema | @reference "tailwindcss" no ve tu tema | Apunta a tu hoja: @reference "./index.css" |
exceeded maximum budget en Angular | @import "tailwindcss" en un componente | Cámbialo por @reference |
| El CSS crece mucho y Preflight aparece repetido | El mismo error, en un framework sin presupuestos | Busca @import "tailwindcss" fuera de la hoja principal |
| Tarjetas de distinta altura en Angular cuando un texto es más largo | El elemento anfitrión se interpone en la rejilla | host: { class: 'grid' } |
| Una clase aparece solo a partir de la segunda compilación | dist no está en .gitignore | Añádelo |
Unexpected token 'export' en postcss.config.js | Falta "type": "module" | Añádelo, o usa postcss.config.mjs |
Una clase de un paquete de node_modules no se genera | Tailwind no lee esa carpeta | @source "../node_modules/paquete" (módulo 01) |
| Un HTML nuevo no recibe estilos con el servidor en marcha | @tailwindcss/vite no lo detecta | Guarda la hoja principal para volver a escanear |
Comparación final
| Framework | Paquete | Aplicar clases condicionales | Estilos de componente |
|---|---|---|---|
| React | @tailwindcss/vite | Plantilla de cadena con clases completas, clsx | CSS Modules con @reference |
| Next.js | @tailwindcss/postcss | Igual que React | CSS Modules con @reference |
| Vue | @tailwindcss/vite | :class="{ 'a b': cond }" | <style scoped> con @reference |
| Svelte | @tailwindcss/vite | class:bg-sky-100={cond} | <style> con @reference |
| Angular | @tailwindcss/postcss | [class.x], [ngClass], [class] | styleUrl con @reference |
13. Ejercicios
Ejercicio 1: Tailwind no aplica
Heredas un proyecto de React. Un botón <button className="boton bg-rose-600"> sale violeta. En DevTools, .bg-rose-600 existe, pero aparece tachada. src/index.css contiene esto:
@import "tailwindcss";
.boton {
background-color: #7c3aed;
}
Explica la causa y corrígelo sin borrar la regla ni usar !.
Ver solución
La regla .boton está fuera de cualquier capa, mientras que bg-rose-600 vive en @layer utilities. El CSS sin capa gana siempre al que está en una capa, aunque tengan la misma especificidad y la utilidad venga después. Por eso no hay nada que arreglar en la clase.
@import "tailwindcss";
@layer components {
.boton {
background-color: #7c3aed;
}
}
La capa components se declara antes que utilities, así que ahora bg-rose-600 gana y .boton queda como valor por defecto. Es el caso de las figuras 3 y 4.
Ejercicio 2: La etiqueta de estado
Corrige este componente, en el que solo algunos estados tienen color:
export function Etiqueta({ estado }: { estado: 'ENVIADO' | 'CANCELADO' }) {
const color = estado === 'ENVIADO' ? 'sky' : 'rose';
return <span className={`bg-${color}-100 text-${color}-800`}>{estado}</span>;
}
Explica por qué un compañero dice que “en su pantalla funciona”.
Ver solución
const estilos = {
ENVIADO: 'bg-sky-100 text-sky-800',
CANCELADO: 'bg-rose-100 text-rose-800',
} as const;
export function Etiqueta({ estado }: { estado: keyof typeof estilos }) {
return <span className={estilos[estado]}>{estado}</span>;
}
bg-${color}-100 no existe como texto en el fichero, de modo que Tailwind no genera nada por ella. A tu compañero le funciona si otro fichero contiene bg-rose-100 entera: la clase existe en el CSS y la plantilla la aprovecha. Es lo que muestra la figura 9. También podría deberse a una carpeta dist fuera de .gitignore.
Ejercicio 3: Barra con valor de la API
Una API devuelve un porcentaje de objetivo de ventas. Crea un componente de React BarraProgreso con un valor numérico y un color cualquiera. Debe funcionar con cualquier número sin generar una clase por valor.
Ver solución
import type { CSSProperties } from 'react';
export function BarraProgreso({ valor, color }: { valor: number; color: string }) {
const variables = { '--avance': `${valor}%`, '--tono': color } as CSSProperties;
return (
<div className="h-3 w-full overflow-hidden rounded-full bg-slate-200" style={variables}>
<div className="h-full w-(--avance) rounded-full bg-(--tono)" />
</div>
);
}
Tailwind genera dos reglas fijas, w-(--avance) y bg-(--tono), que leen la variable. Cada barra aporta su valor en style. En TypeScript hace falta as CSSProperties, porque las propiedades personalizadas no están en ese tipo. En Tailwind 3.4, escribe w-[--avance] y bg-[--tono].
Ejercicio 4: Un CSS Module con el tema
Crea Aviso.module.css para que use text-marca, definido en src/index.css, y añade un cambio de borde al pasar el ratón con @variant. Indica qué errores obtendrías sin @reference y con @reference "tailwindcss".
Ver solución
@reference "./index.css";
.aviso {
@apply rounded-lg border border-amber-300 bg-amber-50 p-4 text-sm text-amber-900;
@variant hover {
@apply border-amber-500;
}
}
.titulo {
@apply font-semibold text-marca;
}
Sin @reference obtienes Cannot apply unknown utility class 'rounded-lg', porque esa hoja no conoce ninguna clase. Con @reference "tailwindcss" obtienes Cannot apply unknown utility class 'text-marca': conoce el tema por defecto, pero no el tuyo.
@variant debe ir anidado dentro de la regla. Lo probamos primero fuera, envolviendo la regla .aviso, y generó un selector erróneo con :scope:hover. La versión anidada genera @media (hover:hover) { ._aviso_15e1w_2:hover { ... } }.
Ejercicio 5: Etiqueta en Angular
Escribe un componente de Angular EtiquetaStock que reciba las existencias con input.required y aplique la clase de color con [class]. Explica por qué no debes escribir [class]="'bg-' + color() + '-100'".
Ver solución
import { Component, computed, input } from '@angular/core';
const estilos = {
alto: 'bg-emerald-100 text-emerald-800',
bajo: 'bg-amber-100 text-amber-800',
agotado: 'bg-rose-100 text-rose-800',
};
@Component({
selector: 'app-etiqueta-stock',
template: `
<span class="shrink-0 rounded-full px-2 py-0.5 text-xs font-medium" [class]="clases()">
{{ nivel() }}
</span>
`,
})
export class EtiquetaStock {
readonly stock = input.required<number>();
protected readonly nivel = computed(() => {
const unidades = this.stock();
return unidades === 0 ? 'agotado' : unidades < 5 ? 'bajo' : 'alto';
});
protected readonly clases = computed(() => estilos[this.nivel()]);
}
El class estático y el enlace [class] se combinan. La expresión con + falla por la misma razón que en React: el escáner lee texto, y la clase completa nunca está escrita.
Ejercicio 6: Botón en Vue
Convierte un <button class="rounded-lg bg-marca px-4 py-2 text-white hover:bg-indigo-800"> de Vue en una clase .boton dentro de <style scoped>. La hoja principal está en src/estilos.css y el componente en src/.
Ver solución
<template>
<button class="boton">Comprar</button>
</template>
<style scoped>
@reference "./estilos.css";
.boton {
@apply rounded-lg bg-marca px-4 py-2 text-white hover:bg-indigo-800;
}
</style>
La variante hover: funciona dentro de @apply. En Tailwind 3.4 quita la línea @reference y el resto queda igual. Recuerda que una regla scoped está fuera de las capas: si quieres cambiar el fondo desde fuera con bg-*, envuelve la regla en @layer components.
Ejercicio 7: Clases que llegan de la API
La API de EMPRESA devuelve por cada categoría una clase de fondo: bg-sky-100, bg-violet-100 o bg-teal-100. No se escriben en ningún fichero. Haz que se generen en Tailwind 4.3 y en Tailwind 3.4.
Ver solución
En Tailwind 4.3:
@import "tailwindcss";
@source inline("bg-{sky,violet,teal}-100");
En Tailwind 3.4:
// tailwind.config.js
export default {
content: ['./index.html', './src/**/*.{ts,tsx}'],
safelist: [{ pattern: /^bg-(sky|violet|teal)-100$/ }],
theme: { extend: {} },
plugins: [],
};
Antes de aceptarlo, plantéate si la API no debería devolver un nombre de categoría y dejar que el código elija las clases con un objeto. Guardar clases de CSS en la base de datos acopla los datos al diseño.
Ejercicio 8: Prettier para un proyecto con cn
Tu proyecto usa una función cn y una hoja en src/index.css. Configura Prettier para que ordene las clases dentro de cn(), y explica por qué const c = 'text-white px-4'; no cambia.
Ver solución
{
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./src/index.css",
"tailwindFunctions": ["cn", "clsx"]
}
El plugin solo ordena lo que sabe que son clases: atributos conocidos (class, className, [ngClass], :class…) y argumentos de las funciones declaradas. Una cadena en una variable es, para él, un texto como cualquier otro. Si la pusiera en orden, podría alterar textos que no son clases.
Ejercicio 9: Fuente propia en Next.js
Haz que font-sans use Inter en un proyecto de Next.js, con next/font/google. Explica para qué sirve inline.
Ver solución
// app/layout.tsx
import type { ReactNode } from 'react';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="es" className={inter.variable}>
<body className="font-sans">{children}</body>
</html>
);
}
/* app/globals.css */
@import "tailwindcss";
@theme inline {
--font-sans: var(--font-inter);
}
Con @theme inline, la utilidad font-sans lleva var(--font-inter) escrito directamente, en vez de pasar por una variable intermedia en :root, y así se resuelve en el elemento que usa la clase. Con la clase de next/font en <html> funcionaría también sin inline, porque ese elemento es :root. Si la mueves a <body>, sin inline dejaría de funcionar. Con inline no dependes de dónde esté.
Ejercicio 10 (propuesto): La tarjeta en Svelte
Crea un proyecto con la plantilla svelte-ts, instala Tailwind y porta TarjetaProducto. Sustituye las seis directivas class: por un objeto de clases completas, y elige qué versión te parece más legible.
Ejercicio 11 (propuesto): Migrar a @tailwindcss/webpack
Toma un proyecto con webpack y postcss-loader, cambia el loader por @tailwindcss/webpack, borra postcss.config.js y comprueba que el CSS generado tiene el mismo tamaño.
Ejercicio 12 (propuesto): Un componente que se deja personalizar
En Angular, crea un componente app-boton con estilos propios que admita class="bg-rose-600" desde el componente padre. Prueba primero con los estilos fuera de una capa, y después dentro de @layer components.
Resumen
| Concepto | API | Idea clave |
|---|---|---|
| React, Vue y Svelte | @tailwindcss/vite | Plugin en vite.config.ts e @import "tailwindcss" en la hoja de entrada |
| Angular | .postcssrc.json, ng add tailwindcss | PostCSS, y la configuración solo en JSON |
| Next.js | postcss.config.mjs | Las clases funcionan igual en Server y Client Components |
| Webpack | @tailwindcss/webpack | Último de la lista de loaders; desde 4.2 |
| Instalación en v3 | tailwind.config.js, @tailwind | PostCSS y content; Angular lo detecta solo |
| CSS sin capa | @layer base, @layer components | Gana a toda utilidad, sin importar la especificidad |
| Hojas de componente | @reference "./index.css" | Lee el tema y no emite nada |
@reference "tailwindcss" | Tema por defecto | No ve tus clases de @theme |
| Importar en un componente | @import "tailwindcss" | Duplica Preflight; en Angular rompe el presupuesto |
| Tema en CSS propio | --theme(--color-x) | Añade valor de reserva y comprueba el nombre |
| Clases dinámicas | `bg-${color}-100` | No funciona: el escáner lee texto y no ejecuta |
| Valores cerrados | Objeto con clases completas | La clase debe estar escrita entera |
| Valores continuos | w-(--avance), style | Clase fija que lee una variable |
| Clases externas | @source inline("..."), safelist | Solo para catálogos pequeños |
| Fuentes en Next.js | next/font, @theme inline | La variable se resuelve donde se usa |
| Angular: anfitrión | host: { class: 'grid' } | El elemento del componente es real |
| Angular: clases | [class.x], [ngClass], [class] | Todas se detectan, incluidas variantes y arbitrarios |
| Vue: clases | :class="{ 'a b': cond }" | Claves con clases completas |
| Svelte: clases | class:bg-sky-100={cond} | La directiva se detecta |
| Prettier | tailwindFunctions, tailwindStylesheet | Debe ser el último plugin |