Módulo 01: Instalación y entorno

Objetivos del módulo

  • Entender que Tailwind es un generador de CSS y qué consecuencias tiene eso en el día a día
  • Crear desde cero el proyecto Vite que usarás en todo el curso
  • Saber qué contiene @import "tailwindcss" y qué capas añade a tu hoja de estilos
  • Compilar sin empaquetador con la CLI @tailwindcss/cli, también en proyectos PHP o Symfony
  • Integrar Tailwind en herramientas basadas en PostCSS con @tailwindcss/postcss
  • Hacer pruebas rápidas sin instalar nada con el CDN @tailwindcss/browser, y saber por qué no sirve en producción
  • Montar el mismo proyecto en la LTS (Tailwind 3.4) con tailwind.config.js, content y @tailwind
  • Predecir qué ficheros lee la detección automática y cuáles ignora
  • Controlar la detección con @source, @source not, @source inline, source() y source(none)
  • Configurar la extensión de VS Code y ordenar clases con Prettier
  • Seguir un método para depurar una clase que no hace efecto

1. Qué vas a instalar exactamente

Tailwind no es una hoja de estilos

Bootstrap es, en esencia, un fichero CSS ya escrito: lo enlazas y tienes miles de reglas disponibles, las uses o no. Tailwind funciona al revés. No trae un fichero con todas las clases. Trae un programa que lee tu proyecto, averigua qué clases estás usando y escribe un CSS que contiene esas y ninguna más.

Esta diferencia explica casi todo lo que verás en este módulo. Por eso necesitas instalar algo, por eso hay un paso de compilación, y por eso aparecen problemas que en CSS normal no existen, como «he escrito la clase bien y no hace nada».

Cuatro pasos: los ficheros se leen como texto, se extraen candidatos, el motor los comprueba y se escribe el CSS final

Figura 1. El recorrido de una clase. El motor no entiende tu HTML ni tu JavaScript: los lee como texto.

El proceso tiene cuatro pasos:

  1. Lectura. Tailwind recorre los ficheros del proyecto y los trata como texto plano. No interpreta HTML ni ejecuta JavaScript.
  2. Candidatos. De ese texto extrae toda palabra que podría ser una clase. Aquí entran bg-indigo-600, pero también const o main.
  3. Comprobación. Cada candidato se contrasta con las utilidades que conoce el motor y con los valores del tema. Lo que no corresponde a nada se descarta sin avisar.
  4. Escritura. Con lo que queda, genera el CSS final y le añade el reinicio de estilos y las variables del tema.

La consecuencia práctica es una regla que repetiremos varias veces: una clase solo existe en el CSS si aparece escrita entera en un fichero que el motor haya leído.

📘 Concepto: Las clases que Tailwind genera no dependen de lo que el navegador ve al ejecutar la página, sino de lo que hay escrito en tus ficheros cuando se compila. Si construyes un nombre de clase en tiempo de ejecución, el compilador nunca lo vio. Lo veremos en la sección 9.

Lo que necesitas antes de empezar

Herramienta Versión Para qué
Node.js 20.19 o superior dentro de la rama 20, o 22.12 o superior Lo exige Vite 8 (campo engines de su package.json)
npm La que trae Node Instalar paquetes
Un editor Usaremos VS Code Autocompletado de clases (sección 11)
Un navegador moderno Safari 16.4+, Chrome 111+ o Firefox 128+ Son los mínimos de Tailwind 4

Comprueba tu versión de Node:

node -v

Si necesitas repasar HTML o CSS, consulta el Curso de HTML y CSS. Si es la primera vez que usas npm o los módulos de JavaScript, tienes la base en el Curso de JavaScript.

⚠️ Importante: Los paquetes de este curso están en estas versiones: tailwindcss, @tailwindcss/vite, @tailwindcss/postcss, @tailwindcss/cli y @tailwindcss/browser en 4.3.3, y vite en 8.3.5. Los cuatro paquetes @tailwindcss/* y tailwindcss se publican juntos: mantenlos en la misma versión.

Cuatro formas de instalarlo

Las cuatro usan el mismo motor. Lo que cambia es quién lanza ese motor:

Vía Paquete Quién lo lanza Cuándo usarla
Plugin de Vite @tailwindcss/vite Vite Proyectos con Vite. Es la vía de este curso
CLI @tailwindcss/cli Tú, desde la terminal Sin empaquetador: PHP, Symfony, HTML estático
Plugin de PostCSS @tailwindcss/postcss PostCSS Herramientas que ya usan PostCSS: Next.js, Angular, Webpack
CDN @tailwindcss/browser El navegador Pruebas y demostraciones, nunca producción

Árbol de decisión con cuatro preguntas que llevan al CDN, al plugin de Vite, al de PostCSS o a la CLI

Figura 2. Cómo elegir. Si dudas, usa Vite: es la opción más sencilla y la que seguiremos en el curso.

El resto de la sintaxis es idéntica. Todo lo que aprendas en este curso funciona igual con cualquiera de las cuatro, porque el fichero CSS de entrada y las clases no cambian. La integración con frameworks concretos (React, Angular, Next.js, Vue) se estudia en el módulo 18.


2. El proyecto del curso con Vite

Vite es una herramienta de desarrollo con dos funciones. Durante el desarrollo sirve tus ficheros y recarga el navegador al guardar. Al terminar, empaqueta todo en una carpeta dist lista para publicar. Vamos a crear con él el proyecto tienda-empresa, que usarás en los siguientes módulos.

Paso 1: crear el proyecto

npm create vite@latest tienda-empresa -- --template vanilla
cd tienda-empresa
npm install

El doble guion -- separa los argumentos de npm de los de create-vite. Sin él, npm se quedaría con --template. La plantilla vanilla es JavaScript puro, sin framework.

La plantilla trae ficheros de demostración que no necesitas. Borra el contenido de ejemplo para partir de un proyecto limpio:

# Desde la carpeta tienda-empresa
rm -rf src/assets src/counter.js src/style.css src/main.js public/icons.svg

💡 Consejo: En Windows, ejecuta esa orden en Git Bash, o borra los ficheros desde el explorador de VS Code. Lo importante es que queden index.html, package.json, .gitignore, public/ y una carpeta src/ vacía.

Paso 2: instalar Tailwind 🆕 v4.0

🆕 Desde Tailwind 4.0. No existe en la LTS (Tailwind 3.4): el paquete @tailwindcss/vite es propio de v4. Si usas la LTS: instala tailwindcss@v3-lts, postcss y autoprefixer, y conecta Tailwind con PostCSS como se explica en la sección 7.

npm install tailwindcss @tailwindcss/vite

Crea en la raíz un fichero vite.config.js:

import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [tailwindcss()],
});

Paso 3: crear la hoja de entrada 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no se usa @import "tailwindcss", sino tres directivas @tailwind. Si usas la LTS: escribe @tailwind base;, @tailwind components; y @tailwind utilities; en tu hoja de entrada.

Crea src/app.css con una sola línea:

@import "tailwindcss";

Esta es la hoja de entrada: el fichero donde más adelante pondrás tu tema y tus utilidades propias. Por ahora, con esa línea basta.

Paso 4: escribir la primera página

Sustituye index.html por esta tarjeta, con un producto real de la tienda:

<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Tienda EMPRESA</title>
    <link rel="stylesheet" href="/src/app.css" />
  </head>
  <body class="min-h-screen bg-slate-100 p-8">
    <article class="mx-auto max-w-sm rounded-xl border border-slate-200 bg-white p-6 shadow-md">
      <p class="text-sm font-semibold text-indigo-600">Portátiles</p>
      <h1 class="mt-1 text-2xl font-bold text-slate-900">Portátil UltraBook 14</h1>
      <p class="mt-2 text-slate-600">Portátil ligero de 14 pulgadas, 16 GB RAM</p>
      <p class="mt-4 text-3xl font-bold text-slate-900">899 €</p>
      <button class="mt-4 w-full rounded-lg bg-indigo-600 px-4 py-2 font-semibold text-white hover:bg-indigo-700">
        Añadir al carrito
      </button>
    </article>
  </body>
</html>

Fíjate en el <link>: la ruta /src/app.css empieza en la raíz del proyecto. Vite intercepta esa petición, pasa el fichero por Tailwind y devuelve el CSS ya generado. El navegador nunca ve la línea @import "tailwindcss".

Aún no hace falta que entiendas cada clase: las estudiarás en los módulos siguientes. Con esta orientación te basta:

Clases Qué hacen Dónde se estudian
p-6, mt-4, px-4, mx-auto, max-w-sm, w-full Espacios y tamaños Módulo 03
text-2xl, font-bold, text-slate-900 Tipografía Módulo 04
bg-white, bg-indigo-600 Color de fondo Módulo 05
rounded-xl, border, shadow-md Bordes y sombras Módulo 06
hover:bg-indigo-700 Cambia el fondo al pasar el ratón Módulo 11

Paso 5: arrancar

npm run dev

Vite te indicará la dirección, normalmente http://localhost:5173/. Ábrela y verás la tarjeta:

Tarjeta de producto con el Portátil UltraBook 14, su descripción, el precio de 899 euros y un botón morado

Figura 3. La primera página. Si ves esto, Tailwind funciona.

Si quitas el <link> de la página, las clases siguen ahí, pero ningún CSS las define. El navegador aplica sus estilos por defecto:

La misma página sin estilos: texto con tipografía serif y un botón gris del sistema

Figura 4. La misma página sin la hoja de estilos. Es lo primero que debes reconocer al depurar: si ves esto, el problema está en la instalación o en el enlace, no en una clase concreta.

Prueba la recarga en caliente: con el servidor en marcha, cambia bg-indigo-600 por bg-emerald-600 en el botón y guarda. El navegador se actualiza solo, y esa clase, que antes no existía en el CSS, ya está generada.

⚠️ Importante: @tailwindcss/vite no detecta los ficheros HTML creados con el servidor en marcha. Si añades una página nueva y sus clases no aparecen, guarda src/app.css para que vuelva a escanear, o reinicia el servidor. Editar ficheros que ya existían funciona sin problemas.

Estructura final

tienda-empresa/
├── .gitignore
├── index.html
├── package.json
├── vite.config.js
├── public/            Ficheros que se sirven tal cual, como /img/escena.svg
└── src/
    └── app.css        Hoja de entrada de Tailwind

La carpeta public/ importa: algunos módulos, como el 06, te pedirán guardar imágenes en public/img/ para usarlas con rutas como /img/teclado.svg.

Hay dos formas válidas de cargar la hoja en un proyecto Vite. La plantilla original usa la segunda:

<!-- Opción 1: en el HTML. La que usamos en el curso -->
<link rel="stylesheet" href="/src/app.css" />
// Opción 2: en src/main.js, cargado con <script type="module">
import './app.css';

La primera es más directa para páginas HTML, porque no necesita JavaScript. La segunda es la habitual en React o Vue, donde todo parte de un módulo JavaScript.

Compilar para producción

npm run build

Esta es la salida real en nuestro proyecto (se resumen las líneas relevantes):

dist/assets/index-C8m7IZrF.css  9.96 kB │ gzip: 2.65 kB
✓ built in 4.16s

En dist/ queda la web lista para publicar. El nombre lleva un hash (C8m7IZrF) que cambia cuando cambia el contenido; así el navegador no reutiliza una versión antigua guardada en caché.

Casi 10 kB para una tarjeta puede parecer mucho, pero esa cifra incluye el reinicio de estilos y las variables del tema. Al crecer el proyecto, el tamaño aumenta despacio, porque cada clase solo se escribe una vez aunque la uses en mil elementos. Lo mediremos en el módulo 20.

Para probar el resultado tal como se publicará:

npm run preview

Opciones del plugin

El plugin de Vite admite una sola opción, según su fichero de tipos:

// optimize: true o false, o bien un objeto con minify
tailwindcss({ optimize: { minify: false } });

Solo la necesitarás para inspeccionar el CSS de producción sin minimizar. En condiciones normales, deja tailwindcss() sin argumentos.


3. Qué hay dentro de @import "tailwindcss"

Una línea no parece gran cosa, pero el paquete contiene un index.css de 950 líneas. Su primera línea declara cuatro capas:

@layer theme, base, components, utilities;

Una capa de cascada (@layer) es una función estándar de CSS que agrupa reglas y fija qué grupo gana a cuál, sin importar la especificidad. Las capas posteriores ganan a las anteriores. El orden declarado significa esto:

Capa Qué contiene Gana a
theme Las variables CSS del tema: colores, tamaños, fuentes Nada
base Preflight, el reinicio de estilos theme
components Vacía de partida: es para tus componentes base
utilities Las clases que usas, como p-4 Todas las anteriores

Gracias a este orden, una utilidad siempre vence a un estilo de la capa base, aunque esa regla tenga un selector más específico.

Para ver el efecto, tomemos este HTML mínimo:

<div class="p-4 bg-indigo-600 hover:bg-indigo-700"></div>

Este es un extracto real del CSS generado por 4.3.3:

@layer theme {
  :root, :host {
    --color-indigo-600: oklch(51.1% 0.262 276.966);
    --color-indigo-700: oklch(45.7% 0.24 277.023);
    --spacing: 0.25rem;
    /* ... */
  }
}

@layer utilities {
  .bg-indigo-600 {
    background-color: var(--color-indigo-600);
  }
  .p-4 {
    padding: calc(var(--spacing) * 4);
  }
  @media (hover: hover) {
    .hover\:bg-indigo-700:hover {
      /* ... */
    }
  }
}

Observa cuatro cosas:

  • .p-4 no contiene 1rem, sino un cálculo: --spacing por 4. La escala completa depende de una sola variable.
  • El tema solo incluye las variables que se usan. No aparece toda la paleta, sino los tonos que necesitas.
  • Los colores están en oklch, un espacio de color moderno que explicaremos en el módulo 05.
  • El estado hover va dentro de @media (hover: hover), de modo que no se activa en pantallas táctiles.

Todo esto se estudia a fondo en el módulo 02. Por ahora quédate con la idea de las capas, porque será clave al depurar en la sección 13.


4. Compilar sin empaquetador: la CLI

@tailwindcss/cli 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) la CLI viene dentro del paquete tailwindcss y se lanza con npx tailwindcss. Si usas la LTS: no instales @tailwindcss/cli; ejecuta npx tailwindcss -i ./src/input.css -o ./dist/salida.css --watch, con las mismas opciones -i, -o, --watch y --minify.

Vite es la mejor opción cuando tu proyecto ya es de JavaScript. Pero si haces una web con PHP, Symfony, Laravel o HTML estático, no necesitas un empaquetador completo solo para el CSS. La CLI hace exactamente eso: lee una hoja de entrada y escribe una de salida.

npm install tailwindcss @tailwindcss/cli

Crea src/app.css con @import "tailwindcss"; y compila:

npx @tailwindcss/cli -i ./src/app.css -o ./public/salida.css

La salida real:

≈ tailwindcss v4.3.3

Done in 1s

La página enlaza el fichero generado, nunca el de entrada:

<link rel="stylesheet" href="/salida.css" />

Este es el error más típico con la CLI: enlazar src/app.css. El navegador recibe @import "tailwindcss", intenta descargar un fichero con ese nombre, falla, y la página se queda sin estilos.

Modo vigilancia

Mientras desarrollas, no querrás recompilar a mano. Con --watch, la CLI se queda escuchando y recompila cuando cambia un fichero:

npx @tailwindcss/cli -i ./src/app.css -o ./public/salida.css --watch

La salida real tras añadir una clase a un HTML, con la CLI ya en marcha:

≈ tailwindcss v4.3.3

Done in 1s
Done in 45ms
Done in 159ms

La primera compilación tarda más porque carga el motor; las siguientes son incrementales y tardan milisegundos. A diferencia de Vite, la CLI no recarga el navegador: tienes que pulsar F5 tú mismo o usar otra herramienta.

Si quieres que siga vigilando aunque se cierre la entrada estándar, algo habitual cuando se lanza desde otro proceso, usa --watch=always.

Todas las opciones

La ayuda de la 4.3.3 (npx @tailwindcss/cli --help) ofrece estas opciones:

Opción Efecto
-i, --input Fichero de entrada
-o, --output Fichero de salida. Por defecto -, la salida estándar
-w, --watch[=always] Recompila al cambiar algo
--poll[=ms] Vigila por sondeo en vez de por eventos del sistema
-m, --minify Optimiza y minimiza
--optimize Optimiza sin minimizar
--cwd Carpeta de trabajo, por defecto .
--map Genera un mapa de fuentes
--silent Oculta la salida que no sea un error
-h, --help Muestra la ayuda

Sobre un proyecto de prueba pequeño, los tamaños medidos fueron estos:

Orden Tamaño
Sin opciones 5022 bytes
--optimize 5741 bytes
--minify 4452 bytes

Curiosamente, en esta prueba --optimize ocupó más que la salida normal: optimizar no significa reducir. Para producción, usa --minify, que según la ayuda optimiza y además minimiza.

Opciones recientes 🆕 v4.3

🆕 Desde Tailwind 4.3. No existe en la LTS (Tailwind 3.4): --silent llegó en la 4.3.1, y el uso conjunto de --watch --poll se incorporó en la 4.3.3. Si usas la LTS: para ocultar la salida redirígela con > /dev/null; la CLI de v3 tiene su propia opción --poll para vigilar por sondeo.

El sondeo sirve en los casos en que el sistema no notifica los cambios en los ficheros, como en carpetas compartidas de máquinas virtuales o de contenedores Docker:

npx @tailwindcss/cli -i ./src/app.css -o ./public/salida.css --watch --poll

--silent es útil en scripts y en integración continua, donde solo te interesan los errores:

npx @tailwindcss/cli -i ./src/app.css -o ./public/salida.css --minify --silent

Scripts de npm

Las órdenes largas se guardan en package.json, para no tener que recordarlas:

{
  "scripts": {
    "dev:css": "npx @tailwindcss/cli -i ./src/app.css -o ./public/salida.css --watch",
    "build:css": "npx @tailwindcss/cli -i ./src/app.css -o ./public/salida.css --minify"
  }
}
npm run dev:css     # Mientras desarrollas
npm run build:css   # Antes de publicar

Añade el fichero generado a .gitignore: se puede reconstruir, y versionarlo solo produce conflictos. Y ten cuidado: al estar en .gitignore, si no tienes un paso de compilación en el servidor, tendrás que generar el fichero en el despliegue.

La CLI y el problema de npx tailwindcss

El paquete @tailwindcss/cli registra un ejecutable llamado tailwindcss, así que, con ese paquete instalado, npx tailwindcss también funciona. Sin él, el paquete tailwindcss de v4 no incluye ningún ejecutable y npm responde con este error real:

npm error could not determine executable to run

Muchos tutoriales antiguos empiezan con npx tailwindcss init, y por eso es un tropiezo frecuente. Lo explicamos en la sección 7.


5. Integración con PostCSS

@tailwindcss/postcss 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) el plugin de PostCSS es el propio paquete tailwindcss, y hace falta autoprefixer. Si usas la LTS: instala tailwindcss@v3-lts postcss autoprefixer y declara tailwindcss: {} y autoprefixer: {} en postcss.config.js.

PostCSS es un programa que transforma CSS mediante plugins. Muchas herramientas lo traen integrado, y entonces la forma natural de añadir Tailwind es como un plugin más.

npm install tailwindcss @tailwindcss/postcss postcss

Crea postcss.config.mjs en la raíz:

export default {
  plugins: {
    '@tailwindcss/postcss': {},
  },
};

Y la hoja de entrada es la misma:

@import "tailwindcss";

Vite lee este fichero sin configuración adicional, así que hemos podido comprobarlo compilando la misma tarjeta sin vite.config.js:

dist/assets/index-Da5gD_Uq.css  8.28 kB │ gzip: 2.39 kB
✓ built in 36.56s

No necesitas postcss-import ni autoprefixer: en v4 el motor resuelve los @import y añade los prefijos por su cuenta, porque integra Lightning CSS.

El error típico

Si dejas la configuración antigua, con tailwindcss como plugin, la compilación se detiene. A diferencia de la mayoría de problemas de este módulo, aquí el mensaje es claro:

Error: [postcss] It looks like you're trying to use `tailwindcss` directly as a PostCSS plugin.
The PostCSS plugin has moved to a separate package, so to continue using Tailwind CSS with
PostCSS you'll need to install `@tailwindcss/postcss`

Dice que el plugin de PostCSS está ahora en otro paquete. La solución es la de arriba.

Opciones del plugin

Su fichero de tipos define tres opciones:

Opción Por defecto Para qué sirve
base La carpeta de trabajo Carpeta desde la que se buscan clases
optimize Activa si NODE_ENV vale production Optimizar y minimizar (true, false o { minify: false })
transformAssetUrls true Reescribir las URL de recursos

Cuándo elegir PostCSS y cuándo el plugin de Vite

Si usas Vite, prefiere @tailwindcss/vite. Es la opción que el proyecto recomienda para Vite y la que necesita menos configuración. Reserva PostCSS para las herramientas que no tienen un plugin propio, como Next.js o Angular, que trataremos en el módulo 18.

Sass, Less y Stylus

La guía oficial es clara: Tailwind 4 no está pensado para usarse con preprocesadores como Sass, Less o Stylus. Su función ya la cubre CSS moderno y el propio Tailwind: variables, anidamiento, importaciones.

Conviene contarlo con precisión. Probamos un fichero mínimo, app.scss, con @import "tailwindcss";, y compiló sin error, con un aviso de que Sass va a retirar @import:

DEPRECATION WARNING [import]: Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0.

Que un caso mínimo funcione no significa que sea una combinación soportada, y ese aviso anuncia que dejará de funcionar. Sass procesa el fichero antes que Tailwind, y tus directivas de Tailwind pasarían por un programa que no las conoce. En un proyecto nuevo, usa CSS. Si tu proyecto depende de Sass y no puedes cambiarlo, esta es una razón legítima para quedarte en v3.


6. Pruebas rápidas: el CDN

@tailwindcss/browser 🆕 v4.0

🆕 Desde Tailwind 4.0. No existe en la LTS (Tailwind 3.4): este paquete es propio de v4. Si usas la LTS: carga <script src="https://cdn.tailwindcss.com"></script>, que en v3 también es solo para desarrollo; en lugar de @theme, se personaliza con tailwind.config = { ... } en otro <script>.

Para probar una idea, enseñar algo en clase o hacer una maqueta rápida, no necesitas instalar nada. Basta con una página HTML:

<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Prueba con el CDN</title>
    <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
    <style type="text/tailwindcss">
      @theme {
        --color-marca: oklch(0.55 0.2 265);
      }
    </style>
  </head>
  <body class="bg-slate-100 p-8">
    <p id="etiqueta" class="mx-auto max-w-sm rounded-xl bg-marca p-6 text-center text-xl font-bold text-white shadow-md">
      Sin instalar nada
    </p>
    <div class="mt-6 flex justify-center">
      <button id="boton" class="rounded-lg bg-white px-4 py-2 font-semibold text-slate-900 shadow">
        Añadir una clase nueva
      </button>
    </div>
    <script>
      // El CDN vigila el DOM: las clases añadidas después también generan CSS
      document.querySelector('#boton').addEventListener('click', () => {
        document.querySelector('#etiqueta').classList.add('rotate-2', 'bg-emerald-600');
      });
    </script>
  </body>
</html>

Puedes abrirla con doble clic, sin servidor. El <style type="text/tailwindcss"> admite la misma sintaxis que tu hoja de entrada, incluido @theme, que estudiarás en el módulo 14.

Rótulo azul con el texto Sin instalar nada y un botón blanco debajo

Figura 5. Sin instalar nada: bg-marca es un color propio definido en la propia página.

Por qué funciona, y por qué no sirve para producción

El script incluye el motor completo de Tailwind. Se ejecuta en el navegador, lee las clases del DOM y escribe el CSS en una etiqueta <style>. En nuestra prueba, Tailwind añadió una hoja de 9037 caracteres a la página.

Además, vigila el DOM. A diferencia de lo que ocurre con el compilador, aquí las clases añadidas con JavaScript sí funcionan, incluso si se construyen en tiempo de ejecución:

El mismo rótulo girado ligeramente tras pulsar el botón

Figura 6. Tras pulsar el botón, rotate-2 se genera sobre la marcha. Observa que el fondo sigue azul: bg-emerald-600 también se ha añadido, pero bg-marca le gana.

La figura tiene un detalle interesante. La etiqueta tiene ya bg-marca y le añadimos bg-emerald-600, y no cambia de color. Esto no es un error del CDN: dos clases que asignan la misma propiedad no ganan por su posición en el atributo class, sino por el orden en el que aparecen en el CSS. Lo estudiarás en el módulo 02.

Las razones para no usarlo en producción son concretas:

Problema Por qué
Descarga innecesaria Cada visitante descarga el motor completo solo para generar un CSS que podrías darle ya hecho
Trabajo en el navegador El CSS se genera en cada visita, en el dispositivo del usuario
Parpadeo inicial Hasta que el script termina, la página puede verse sin estilos
Dependencia de un tercero Si el CDN falla, tu web se queda sin diseño

Además, la dirección con @4 siempre apunta a la última 4.x, por lo que tu página puede cambiar sola. Si necesitas una prueba estable, fija la versión: @tailwindcss/browser@4.3.3.

⚠️ Importante: En v3 el equivalente es https://cdn.tailwindcss.com. Esa dirección redirige hoy a la 3.4.17, no a la 3.4.19 que instalas con npm. Si comparas resultados entre el CDN y tu proyecto v3, tenlo en cuenta.


7. La LTS: el mismo proyecto en Tailwind 3.4

Qué significa «LTS» en Tailwind

Tailwind no tiene una política formal de soporte a largo plazo. Lo que existe es una etiqueta de npm, v3-lts, que hoy apunta a 3.4.19. Puedes verlo tú mismo:

npm view tailwindcss dist-tags
{
  next: '4.0.0',
  'v3-lts': '3.4.19',
  latest: '4.3.3',
  insiders: '0.0.0-insiders.410a465'
}

La guía oficial de actualización recomienda quedarse en v3.4 a quien necesite navegadores antiguos, y esa es la principal razón para elegirla: v4 exige Safari 16.4+, Chrome 111+ y Firefox 128+. Tienes el análisis completo en el módulo 00.

Como la instalación es el punto donde más cambia todo, esta sección reúne el montaje completo en v3. Es un apartado autónomo: si usas v4, léelo para entender los tutoriales antiguos que encontrarás; si usas la LTS, este es tu punto de partida en lugar de la sección 2.

Instalación con Vite en v3 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) hacen falta un fichero tailwind.config.js, la opción content y PostCSS. Si usas la LTS: sigue esta sección completa en lugar de la sección 2; el resto del curso te indicará las diferencias en cada apartado marcado.

Crea el proyecto Vite igual que antes y después instala:

npm install -D tailwindcss@v3-lts postcss autoprefixer
npx tailwindcss init -p

La etiqueta @v3-lts es esencial. Sin ella, npm instalaría v4 y la orden siguiente fallaría (sección 4).

La salida real:

Created Tailwind CSS config file: tailwind.config.js
Created PostCSS config file: postcss.config.js

Esos ficheros se generan como módulos ES si tu package.json tiene "type": "module", como ocurre en la plantilla de Vite:

/** @type {import('tailwindcss').Config} */
export default {
  content: [],
  theme: {
    extend: {},
  },
  plugins: [],
}
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

En un proyecto CommonJS aparecen con module.exports, que es lo que verás en muchos tutoriales.

El paso crítico es rellenar content en tailwind.config.js. En v3 no hay detección automática, así que debes decir dónde buscar:

export default {
  content: ['./index.html', './src/**/*.{html,js}'],
  theme: {
    extend: {},
  },
  plugins: [],
}

Crea src/app.css con las tres directivas:

@tailwind base;
@tailwind components;
@tailwind utilities;

El index.html de la sección 2 sirve sin cambios. Compilado con 3.4.19:

dist/assets/index-Q7DCqtg_.css  5.27 kB │ gzip: 1.81 kB

Si olvidas content, v3 al menos te avisa. Esta es la salida real con content: []:

warn - No utility classes were detected in your source files. If this is unexpected,
double-check the `content` option in your Tailwind CSS configuration.
warn - https://tailwindcss.com/docs/content-configuration

La CLI en v3

En v3 no hay paquete aparte. La CLI es parte de tailwindcss:

npx tailwindcss -i ./src/app.css -o ./dist/salida.css
npx tailwindcss -i ./src/app.css -o ./dist/salida.css --watch
npx tailwindcss -i ./src/app.css -o ./dist/salida.css --minify

Equivalencias de instalación

Tarea Tailwind 4.3 Tailwind 3.4
Instalar con Vite tailwindcss @tailwindcss/vite tailwindcss@v3-lts postcss autoprefixer
Conectar con Vite plugins: [tailwindcss()] en vite.config.js postcss.config.js
Hoja de entrada @import "tailwindcss"; @tailwind base;, components, utilities
Fichero de configuración No hace falta tailwind.config.js
Dónde buscar clases Automático, y @source content
Excluir rutas @source not "ruta" content: ['!./ruta/**']
Forzar clases @source inline("clase") safelist
CLI @tailwindcss/cli npx tailwindcss
Plugin de PostCSS @tailwindcss/postcss tailwindcss
Prefijos de navegador Integrados autoprefixer
CDN @tailwindcss/browser@4 cdn.tailwindcss.com
Personalizar @theme en CSS theme.extend en JS

Mezclar versiones: tres errores

1. Directivas de v3 en un proyecto v4. Es el error más traicionero, porque no da ningún error. Con esta hoja en v4:

@tailwind base;
@tailwind components;
@tailwind utilities;

Y este HTML:

<div class="mt-[101px] bg-indigo-600 p-4 rounded-lg"></div>

La compilación termina con normalidad y genera 378 bytes. Solo aparece mt-[101px]. Desaparecen bg-indigo-600, p-4, rounded-lg, Preflight y todas las variables. El motor procesa @tailwind utilities, pero como no se ha cargado el tema no sabe qué es indigo-600, y solo puede generar lo que no depende del tema, como los valores arbitrarios entre corchetes.

El síntoma es una página casi sin estilos. La solución: usa @import "tailwindcss";.

2. @import "tailwindcss" en un proyecto v3. La compilación falla en postcss-import, porque el paquete de v3 no ofrece esa hoja. La solución: usa las tres directivas @tailwind.

3. Un tailwind.config.js que no hace nada en v4. Si heredas un fichero de configuración, v4 lo ignora. En nuestra prueba, un color marca definido ahí no generó bg-marca. Solo funcionó tras enlazarlo explícitamente desde el CSS:

@import "tailwindcss";
@config "../tailwind.config.js";

Esta directiva facilita las migraciones. Los detalles están en el módulo 15 y en el módulo 19.

Por qué el HTML no basta para migrar

La misma página, compilada con las dos versiones, se ve casi igual en la tarjeta, pero no en todo:

Comparación de la misma página en Tailwind 3.4.19 y 4.3.3: la tarjeta es igual, el borde sin color es gris en v3 y rojo en v4, y ring es grueso en v3 y fino en v4

Figura 7. El mismo HTML en las dos versiones. Con los colores explícitos no hay diferencias apreciables, pero border sin color y ring cambian sin dar ningún error.

Son falsos amigos: clases que existen en las dos versiones con distinto resultado. border usa el gris 200 en v3 y currentColor en v4, y por eso el borde se vuelve rojo como el texto. ring pasa de 3 px a 1 px. Los verás marcados a lo largo del curso, y reunidos en el módulo 19.


8. Cómo funciona la detección automática

Detección automática 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no hay detección automática: solo se leen los ficheros que indiques en content. Si usas la LTS: declara en tailwind.config.js algo como content: ['./index.html', './src/**/*.{html,js}'], y amplíalo cuando añadas carpetas.

En v4 no le dices a Tailwind dónde buscar: recorre la carpeta de trabajo por su cuenta. La documentación enumera qué ignora, pero para saber exactamente cómo se comporta montamos un experimento. Pusimos una clase distinta en cada tipo de fichero y miramos cuáles aparecían en el CSS.

Dónde estaba la clase ¿Se generó? Por qué
index.html Sí Fichero normal
src/main.js, dentro de una cadena Sí Fichero normal
Dentro de un comentario HTML <!-- --> Sí Es texto plano
Dentro de un comentario // de JavaScript Sí Es texto plano
README.md Sí Es texto plano
datos.json Sí Es texto plano
notas.txt Sí Es texto plano
dibujo.svg Sí Fichero de texto
.oculta/x.html, en carpeta oculta Sí No se excluye por empezar por punto
legado/viejo.html Sí Cualquier subcarpeta
Comentario dentro de src/app.css No Los ficheros CSS se excluyen
dist/ignorado.html, con dist en .gitignore No Respeta .gitignore
node_modules/lib/x.js No Excluido siempre
package-lock.json No Ficheros de bloqueo
.env No Excluido, incluso con @source
foto.png No Binario
../compartido/x.html No Fuera de la carpeta base

Dos columnas con lo que se lee y lo que se ignora, y una franja con la sintaxis de @source

Figura 8. Resumen del experimento anterior.

De esta tabla salen varias lecciones prácticas:

Los comentarios cuentan. Si comentas un bloque de HTML, sus clases siguen en el CSS final. No es grave, pero explica por qué a veces encuentras en el CSS clases que parecen no usarse.

La documentación también cuenta. Un README.md que hable de bg-red-500 genera esa clase. Tampoco suele ser un problema, pero te ayudará a entender tamaños inesperados.

.gitignore funciona aunque no haya repositorio. Repetimos la prueba con y sin carpeta .git, y dist/ se ignoró en los dos casos. Por eso no conviene añadir a .gitignore carpetas que contienen código fuente de plantillas.

La carpeta base importa. Por defecto, el punto de partida es la carpeta de trabajo. En un monorepo, si lanzas la orden desde la raíz, esa será la base. Para fijarla, usa source(), que verás en la sección 10.

Tailwind no se equivoca por leer de más. Una palabra como flex en un texto genera la clase flex. Es inofensivo, porque una clase generada de más solo añade unos bytes.


9. Clases dinámicas: el error más común

Volvamos a la regla de la sección 1: la clase tiene que estar escrita entera en un fichero. Este código parece correcto y no funciona:

const color = 'emerald';

// Mal: el fichero solo contiene el texto "bg-${color}-300"
elemento.className += ` bg-${color}-300`;

El navegador recibe sin problema la clase bg-emerald-300 en el elemento. Pero el CSS no tiene esa regla, porque el compilador nunca ha visto esas letras juntas: leyó bg-${color}-300, lo descartó, y no ejecuta tu código.

La solución es escribir las clases completas, aunque parezca repetitivo. Un objeto que traduce valores a clases es el patrón habitual:

const color = 'emerald';

// Bien: la clase aparece escrita entera en el fichero
const colores = {
  emerald: 'bg-emerald-600 text-white',
  rose: 'bg-rose-600 text-white',
};

elemento.className += ` ${colores[color]}`;

La figura compara este caso con otros dos errores que parecen iguales desde fuera:

Lista de cinco filas. La 1 es verde, la 2 y la 3 no tienen fondo, la 4 es morada opaca y la 5 es morada semitransparente

Figura 9. Cinco casos reales. Las filas 2, 3 y 4 fallan, y ninguna produce un error ni un aviso en la compilación.

Fila Código Resultado Motivo
1 Objeto con clases completas Funciona El compilador las ve
2 `bg-${color}-300` Sin fondo La clase nunca se escribió entera
3 bg-indgo-600 Sin fondo Error tipográfico: se descarta en silencio
4 bg-indigo-600 bg-opacity-50 Opaco bg-opacity-* se eliminó en v4
5 bg-indigo-600/50 Semitransparente Sintaxis correcta en v4

La fila 4 es la más engañosa: bg-indigo-600 funciona y bg-opacity-50 no existe, así que el botón parece bien y solo pierde la transparencia. La extensión de VS Code te ayudará con estos casos (sección 11).

⚠️ Importante: El mismo problema aparece con cualquier lenguaje de plantillas. En Twig, PHP, JSX o Angular, class="text-{{ estado }}-600" falla por el mismo motivo. Escribe la clase entera en cada rama: {{ error ? 'text-red-600' : 'text-green-600' }}.

El tema se retoma en el módulo 18, con ejemplos en React.


10. Controlar la detección: @source

La detección automática acierta en la mayoría de proyectos. Pero hay tres situaciones en las que tienes que intervenir: añadir algo que se ignora, excluir algo que se lee y forzar clases que no están en ningún fichero.

Añadir rutas con @source 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no existe @source. Si usas la LTS: añade la ruta a la lista content de tailwind.config.js, por ejemplo './node_modules/kit-ui/**/*.js'.

El caso típico es una biblioteca de componentes instalada en node_modules que usa clases de Tailwind. Supón que el paquete kit-ui exporta las clases de un botón:

// node_modules/kit-ui/boton.js
export const clasesBoton = 'rounded-full bg-teal-700 px-6 py-3 text-lg font-bold text-amber-200 shadow-lg';

Como node_modules se excluye, esas clases no llegan al CSS. Tu proyecto importa el paquete, el botón recibe las clases, y no se ve ningún estilo. Se arregla registrando la carpeta:

@import "tailwindcss";
@source "../node_modules/kit-ui";

Dos botones Comprar: sin @source es texto negro sin formato, con @source es una píldora verde con texto amarillo

Figura 10. El mismo botón con y sin @source.

⚠️ Importante: La ruta de @source es relativa a la hoja de estilos, no a la raíz del proyecto. Como app.css está en src/, hay que subir un nivel con ../.

Comprobamos que @source también rescata lo demás que se ignora, salvo una excepción:

Directiva Efecto en el experimento
@source "../../compartido"; Se genera la clase de la carpeta exterior
@source "../node_modules/lib"; Se genera la clase de esa carpeta
@source "../dist"; Se genera, aunque esté en .gitignore
@source "../.env"; No se genera: .env sigue excluido

Fijar la carpeta base con source() 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no existe la función source(). Si usas la LTS: las rutas de content son relativas a la carpeta desde la que lanzas la orden; para que lo sean al fichero de configuración, usa content: { relative: true, files: ['./src/**/*.html'] }.

Puedes indicar dónde empieza la búsqueda en la propia línea de importación:

@import "tailwindcss" source("../src");

En nuestra prueba, de todas las clases del experimento solo se generó la de src/main.js. Es útil en monorepos o cuando las órdenes se lanzan desde otra carpeta.

Desactivar la detección con source(none) 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no existe source(none), porque la lista content ya es explícita. Si usas la LTS: tu lista content tiene exactamente ese efecto: solo se lee lo que aparece en ella.

Si prefieres control total, apaga la detección y enumera las rutas:

@import "tailwindcss" source(none);
@source "../templates";
@source "../src";

Es la opción ideal para Symfony o Laravel: garantiza que no se lean vendor/, var/ ni las carpetas de compilación. También sirve cuando tienes dos hojas, por ejemplo una para la web pública y otra para el panel de administración, y quieres que cada una contenga solo sus clases.

Las plantillas de cualquier lenguaje funcionan, porque se leen como texto. En nuestra prueba se generaron las clases de un base.html.twig y de un Vista.php.

Excluir rutas con @source not 🆕 v4.1

🆕 Desde Tailwind 4.1. No existe en la LTS (Tailwind 3.4). Si usas la LTS: añade la ruta negada a content, como en content: ['./src/**/*.html', '!./src/legado/**']; lo hemos comprobado en 3.4.19 y excluye la carpeta.

@import "tailwindcss";
@source not "../legado";

Sirve para carpetas grandes que sabes que no usan Tailwind: código antiguo, bibliotecas copiadas al proyecto o documentación con muchos ejemplos.

Forzar clases con @source inline 🆕 v4.1

🆕 Desde Tailwind 4.1. No existe en la LTS (Tailwind 3.4). Si usas la LTS: usa safelist en tailwind.config.js, por ejemplo safelist: ['underline', { pattern: /^bg-(emerald|amber|rose)-100$/, variants: ['hover'] }].

A veces una clase no está en ningún fichero del proyecto: por ejemplo, llega de un CMS o de una base de datos. @source inline() la genera igualmente:

@import "tailwindcss";
@source inline("underline");

La cadena admite expansión de llaves, como en Bash, para generar familias enteras:

@import "tailwindcss" source(none);
@source inline("{hover:,}bg-red-{100,500}");

Genera exactamente cuatro clases, según la salida real:

.bg-red-100 { /* ... */ }
.bg-red-500 { /* ... */ }
.hover\:bg-red-100:hover { /* ... */ }
.hover\:bg-red-500:hover { /* ... */ }

La coma tras hover: es importante: {hover:,} significa «con hover: o con nada». Y puedes combinar varias llaves. Esta línea genera las 12 combinaciones de fondo y texto, en tres colores y dos tonos:

@source inline("{bg,text}-{emerald,amber,rose}-{100,800}");

💡 Consejo: Antes de recurrir a @source inline, pregúntate si no bastaría con un objeto de clases completas (sección 9). Es más fácil de mantener, porque las clases están junto al código que las usa. Resérvalo para clases que de verdad vienen de fuera.

Lo contrario también existe: @source not inline("mt-[102px]") impide que se genere una clase aunque aparezca en tus ficheros. Es poco frecuente, pero puede servir para vetar clases concretas.

Resumen de directivas

Necesidad Directiva
Leer algo que se ignora @source "ruta";
No leer una carpeta @source not "ruta";
Cambiar la carpeta base @import "tailwindcss" source("ruta");
Listar todo a mano @import "tailwindcss" source(none);
Generar clases concretas @source inline("clases");
Vetar clases concretas @source not inline("clases");

11. Extensión de VS Code

Sin ayuda del editor, escribir clases es trabajar a ciegas: una errata no da error. La extensión oficial, Tailwind CSS IntelliSense (identificador bradlc.vscode-tailwindcss), resuelve esto.

Instálala desde el panel de extensiones o con esta orden:

code --install-extension bradlc.vscode-tailwindcss

Según su documentación, ofrece:

  • Autocompletado de clases, con muestras de color.
  • Vista previa: al pasar el ratón sobre una clase, muestra el CSS que genera.
  • Comprobación de errores en el CSS y en el marcado, como clases en conflicto.
  • Resaltado de sintaxis de las directivas propias de Tailwind.

La vista previa es muy útil mientras aprendes: te enseña el CSS real sin salir del editor.

Ajustes recomendados

Abre .vscode/settings.json en tu proyecto y añade:

{
  "files.associations": {
    "*.css": "tailwindcss"
  },
  "editor.quickSuggestions": {
    "strings": "on"
  }
}
Ajuste Para qué sirve
files.associations Abre los .css en modo Tailwind, para que el editor reconozca @theme, @source o @utility y no los marque como errores
editor.quickSuggestions con strings Activa las sugerencias dentro de cadenas de texto, como un atributo class en JSX

Cómo encuentra tu hoja en v4 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) la extensión busca el fichero tailwind.config.js. Si usas la LTS: no hace falta nada si ese fichero está en el proyecto; la opción tailwindCSS.experimental.configFile debe apuntar a él si tiene otro nombre o ubicación.

En v4 no hay fichero de configuración, así que la extensión necesita un .css que importe Tailwind. Si no te ofrece sugerencias, indícale cuál es:

{
  "tailwindCSS.experimental.configFile": "src/app.css"
}

Si tu proyecto tiene varias hojas, usa un objeto en el que cada clave sea una hoja y cada valor, un patrón con sus ficheros:

{
  "tailwindCSS.experimental.configFile": {
    "src/publica.css": "src/publica/**",
    "src/admin.css": "src/admin/**"
  }
}

Otros ajustes útiles

Ajuste Para qué sirve Ejemplo
tailwindCSS.includeLanguages Trata otro lenguaje como uno conocido { "plaintext": "html" }
tailwindCSS.classAttributes Atributos que contienen clases. Por defecto: class, className, ngClass, class:list Añadir el tuyo
tailwindCSS.classFunctions Funciones cuyos argumentos son clases ["tw", "clsx"]

La comprobación de errores se ajusta con las opciones tailwindCSS.lint.*, con los valores ignore, warning o error. Dos de ellas te interesarán pronto:

Opción Por defecto Qué detecta
tailwindCSS.lint.cssConflict warning Dos clases que asignan la misma propiedad
tailwindCSS.lint.suggestCanonicalClasses warning Clases con una forma más canónica

💡 Consejo: Si aparece una advertencia Unknown at rule @theme u otra parecida, es la validación de CSS de VS Code, que no conoce Tailwind. Se resuelve con el ajuste files.associations de arriba.

Otros editores: Cursor admite las mismas extensiones que VS Code; Zed trae soporte integrado, y los IDE de JetBrains, como WebStorm o PhpStorm, incluyen el autocompletado de Tailwind.


12. Ordenar clases con Prettier

Con muchas clases, el atributo class se vuelve difícil de leer si cada persona las escribe en un orden distinto. El plugin oficial prettier-plugin-tailwindcss las ordena automáticamente con un criterio fijo. Usaremos Prettier 3.9.10 y el plugin 0.8.1.

npm install -D prettier prettier-plugin-tailwindcss

Crea .prettierrc en la raíz:

{
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindStylesheet": "./src/app.css"
}

La opción tailwindStylesheet indica cuál es tu hoja de entrada. Su ruta es relativa al fichero de configuración de Prettier.

Para este ejemplo, nuestro app.css define un color propio:

@import "tailwindcss";

@theme {
  --color-marca: #123456;
}

Y partimos de un botón con las clases desordenadas, entre las que hay una clase propia, mi-boton:

<button class="hover:bg-marca text-white px-4 sm:px-8 mi-boton py-2 bg-indigo-600 rounded-lg font-semibold">Añadir</button>

Ordena con:

npx prettier --write tarjeta.html

El resultado real:

<button
  class="mi-boton rounded-lg bg-indigo-600 px-4 py-2 font-semibold text-white hover:bg-marca sm:px-8"
>
  Añadir
</button>

El orden sigue una lógica:

  1. Primero, las clases que no son de Tailwind, como mi-boton.
  2. Después, las utilidades, en el orden en el que aparecen en el CSS generado. Las de maquetación van antes que las decorativas.
  3. Al final, las variantes: hover: y luego las de puntos de ruptura, como sm:.

Este orden no es solo estético. Coincide con el orden en el que se aplican las reglas, de modo que la clase que aparece al final tiende a ser la que gana.

Por qué tailwindStylesheet es necesario 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) el plugin busca tailwind.config.js junto a la configuración de Prettier. Si usas la LTS: no pongas tailwindStylesheet; si el fichero tiene otra ubicación, indica "tailwindConfig": "./ruta/tailwind.config.js".

Probamos a quitar esa opción, y el resultado cambió:

<button
  class="hover:bg-marca mi-boton rounded-lg bg-indigo-600 px-4 py-2 font-semibold text-white sm:px-8"
>
  Añadir
</button>

Sin la hoja, el plugin no sabe que marca es un color de tu tema. Trata hover:bg-marca como una clase desconocida y la lleva al principio. No hay ningún error: simplemente, el orden es peor. Si ves clases de tu tema al principio del atributo, revisa esta opción.

Comprobar sin modificar

npx prettier --check .

Con una clase mal ordenada, la salida real fue esta:

Checking formatting...
[warn] tarjeta.html
[warn] Code style issues found in the above file. Run Prettier with --write to fix.

Y tras ordenar:

All matched files use Prettier code style!

Es la orden que se suele lanzar en integración continua.

Otras opciones del plugin

Opción Efecto
tailwindFunctions Ordena también las cadenas pasadas a estas funciones
tailwindAttributes Ordena otros atributos, además de class
tailwindPreserveWhitespace Conserva los espacios entre clases
tailwindPreserveDuplicates Conserva las clases duplicadas

Por ejemplo, con "tailwindFunctions": ["clsx"], esta línea:

const c = clsx("text-white p-4 bg-red-500 p-4");

queda así, ordenada y sin el p-4 repetido:

const c = clsx("bg-red-500 p-4 text-white");

⚠️ Importante: Este plugin debe ser el último de la lista plugins si usas varios, porque se apoya en APIs de Prettier que solo un plugin puede usar a la vez.

Formatear al guardar

Instala la extensión de Prettier (esbenp.prettier-vscode) y añade a .vscode/settings.json:

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode"
}

13. Depurar una clase que no hace efecto

Tailwind no te avisa de que una clase no existe: la descarta. Por eso necesitas un método. Sigue siempre el mismo orden, de lo general a lo concreto. Así evitas pasar media hora revisando una clase cuando el problema es que el CSS no carga.

Diagrama de cuatro preguntas que llevan a cuatro causas: instalación, JavaScript o plantilla, detección o nombre, y cascada

Figura 11. El método de depuración. Cada pregunta descarta un grupo de causas.

Paso 1: ¿falla todo o solo una clase?

Si la página se parece a la figura 4, con fuentes del navegador y sin estilos, el problema no es una clase, sino la instalación:

Síntoma Causa Solución
Sin ningún estilo, sin errores Falta el <link> o el import Enlaza la hoja de entrada
Con la CLI, sin estilos Enlazas src/app.css en lugar de la salida Enlaza el fichero generado
Con la CLI, faltan clases nuevas No está en marcha --watch Vuelve a compilar o usa --watch
Solo algunas clases, sin Preflight @tailwind en un proyecto v4 Usa @import "tailwindcss";
Vite avisa Unknown at rule: @tailwind; hay Preflight, pero ninguna utilidad Falta plugins: [tailwindcss()] en vite.config.js Añádelo
Error de PostCSS al compilar tailwindcss como plugin de PostCSS en v4 Usa @tailwindcss/postcss
could not determine executable to run npx tailwindcss init con v4 No hace falta en v4; en v3, instala @v3-lts
En v3, sin ninguna utilidad y con aviso content vacío Rellena content

Para saber si el CSS llega al navegador, abre las herramientas de desarrollo con F12, ve a Red, recarga y busca el fichero CSS. Un 404 indica una ruta mal escrita. Si abres el fichero y ves @import "tailwindcss" o @tailwind utilities, Tailwind no lo ha procesado.

El caso del plugin olvidado merece una nota, porque engaña. Sin el plugin, Vite encuentra el paquete tailwindcss en node_modules e incrusta su index.css como si fuera una hoja normal. En nuestra prueba, la compilación terminó con un aviso y un CSS de 21,59 kB, más del doble de lo normal, con Preflight y sin ninguna utilidad. La página pierde los márgenes y las fuentes del navegador, pero ninguna clase hace efecto.

Paso 2: ¿tiene el elemento esa clase?

Con F12, en la pestaña Elementos, selecciona el elemento. Si la clase no está en su atributo class, Tailwind no tiene nada que ver: el problema está en tu JavaScript o en tu plantilla, como una condición que no se cumple.

Paso 3: ¿existe la regla en el CSS?

Si la clase está en el elemento pero el panel Estilos no muestra ninguna regla con ese nombre, el CSS no la contiene. Las causas posibles son estas:

Causa Cómo reconocerla Solución
Nombre construido dinámicamente El código fuente no contiene la clase entera Objeto de clases completas (sección 9)
Errata La extensión no la colorea ni muestra su CSS Corrígela
Clase de otra versión Viene de un tutorial antiguo, como bg-opacity-50 Búscala en la tabla del módulo 19
Fichero no leído Está en node_modules, en .gitignore o fuera de la base @source
Fichero nuevo con Vite en marcha Solo falla en páginas recién creadas Guarda app.css
Función de una versión posterior Depende de tu versión instalada Revisa con npm ls tailwindcss

El comprobador más rápido: busca la clase en el CSS generado. En Vite, usa npm run build y busca en dist/assets/. Con la CLI, escribe la salida en un fichero y búscala en él.

Paso 4: ¿la regla existe, pero pierde? 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) @layer no produce capas nativas de CSS, así que decide la especificidad: .con-capa a también vence a text-indigo-600, y en 3.4.19 los dos enlaces de este ejemplo salen rojos. Si usas la LTS: usa en tu CSS selectores de elemento o de una sola clase, o marca la utilidad como importante con !text-indigo-600.

Si Estilos muestra la regla tachada, sí existe, pero otra le gana. Normalmente es tu propio CSS. Aquí hay una trampa de las capas de la sección 3: el CSS que no está en ninguna capa gana a todo lo que sí lo está, sin importar la especificidad:

@import "tailwindcss";

/* Sin capa: gana a TODAS las capas, sea cual sea su especificidad */
.sin-capa a {
  color: crimson;
}

/* En la capa base: pierde contra la capa utilities */
@layer base {
  .con-capa a {
    color: crimson;
  }
}
<p class="sin-capa"><a class="text-indigo-600">este enlace pide text-indigo-600</a></p>
<p class="con-capa"><a class="text-indigo-600">este enlace pide text-indigo-600</a></p>

Dos líneas. En la primera, sin capa, el enlace sale rojo; en la segunda, en @layer base, sale morado

Figura 12. El mismo color, con la misma regla: la única diferencia es la capa. El CSS sin capa vence a la utilidad.

Por eso, al añadir CSS propio a un proyecto con Tailwind, mételo en una capa:

@layer base {
  /* Estilos por defecto de elementos: a, h1, etc. */
}

@layer components {
  /* Tus componentes: .tarjeta, .boton */
}

Esta situación es muy común cuando se integra Tailwind en un proyecto existente: unas hojas antiguas con a { color: ... } o h1 { ... } anulan las utilidades.

La otra causa frecuente son dos clases que asignan la misma propiedad. Ya lo viste en la figura 6: no gana la que escribes después en el atributo, sino la que va después en el CSS. La extensión lo marca con cssConflict. Quédate con una sola de las dos.

Lista de comprobación rápida

# ¿Qué versión tengo realmente instalada?
npm ls tailwindcss

# ¿Qué etiquetas hay publicadas?
npm view tailwindcss dist-tags

# ¿Se genera la clase? Compila a un fichero y búscala
npx @tailwindcss/cli -i ./src/app.css -o ./prueba.css

14. Ejercicios

✏️ Ejercicio: Las soluciones se han compilado y ejecutado en Tailwind 4.3.3, salvo la del ejercicio 7, que es para la 3.4.19.

Ejercicio 1: Proyecto desde cero

Sin copiar de la sección 2, crea el proyecto tienda-empresa, instala Tailwind con Vite y comprueba que funciona pintando un <h1> con el texto «Tienda EMPRESA» de color text-indigo-600 y tamaño text-4xl. Después genera la versión de producción y localiza el fichero CSS.

Ver solución
npm create vite@latest tienda-empresa -- --template vanilla
cd tienda-empresa
npm install
npm install tailwindcss @tailwindcss/vite

vite.config.js:

import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [tailwindcss()],
});

src/app.css:

@import "tailwindcss";

index.html:

<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <title>Tienda EMPRESA</title>
    <link rel="stylesheet" href="/src/app.css" />
  </head>
  <body>
    <h1 class="text-4xl text-indigo-600">Tienda EMPRESA</h1>
  </body>
</html>
npm run dev     # Comprueba el resultado en el navegador
npm run build   # El CSS queda en dist/assets/index-<hash>.css

Las tres piezas imprescindibles son el plugin en vite.config.js, el @import en la hoja y el <link> en la página. Si falta cualquiera, verás una página sin estilos o sin procesar.

Ejercicio 2: Ficha de empleado

Crea una ficha de la primera empleada de la API de EMPRESA: Ana García López, de Dirección, con el correo ana.garcia@empresa.es. Toma como modelo la tarjeta de producto y usa el color emerald en lugar de indigo. El correo debe ser un enlace mailto: con forma de botón.

Ver solución
<body class="bg-slate-100 p-8">
  <article class="mx-auto max-w-sm rounded-xl bg-white p-6 shadow-md">
    <p class="text-sm font-semibold text-emerald-700">Departamento de Dirección</p>
    <h1 class="mt-1 text-2xl font-bold text-slate-900">Ana García López</h1>
    <p class="mt-2 text-slate-600">Directora general</p>
    <a href="mailto:ana.garcia@empresa.es" class="mt-4 block rounded-lg bg-emerald-700 px-4 py-2 text-center font-semibold text-white hover:bg-emerald-800">
      Enviar correo
    </a>
  </article>
</body>

Ficha de Ana García López, directora general, con un botón verde para enviar correo

Figura 13. La ficha terminada.

Un <a> es por defecto un elemento en línea, así que no ocupa todo el ancho. Por eso añadimos block, que un <button> con w-full no necesitaba, y text-center, porque el texto de un enlace no se centra solo.

Ejercicio 3: Web estática con la CLI

Tienes una web HTML sin empaquetador. Añade a package.json dos scripts: dev:css, que vigile los cambios, y build:css, que genere el CSS minimizado. La entrada es src/app.css y la salida public/salida.css. ¿Qué fichero debes enlazar en el HTML?

Ver solución
npm install tailwindcss @tailwindcss/cli
{
  "scripts": {
    "dev:css": "npx @tailwindcss/cli -i ./src/app.css -o ./public/salida.css --watch",
    "build:css": "npx @tailwindcss/cli -i ./src/app.css -o ./public/salida.css --minify"
  }
}

En el HTML se enlaza la salida, nunca la entrada:

<link rel="stylesheet" href="/salida.css" />

Si enlazas src/app.css, el navegador intentará cargar un fichero llamado tailwindcss y fallará, porque nadie ha procesado esa hoja.

En v3 sería igual, cambiando npx @tailwindcss/cli por npx tailwindcss y sin instalar @tailwindcss/cli.

Ejercicio 4: Proyecto Symfony

Tu proyecto tiene esta estructura. Quieres que Tailwind lea solo templates/ y src/, y genere el CSS en public/build/app.css. Escribe la hoja de entrada y la orden de compilación.

mi-proyecto/
├── assets/app.css
├── public/build/     CSS generado
├── src/              Código PHP
├── templates/        Plantillas Twig
└── vendor/           Dependencias
Ver solución

assets/app.css:

@import "tailwindcss" source(none);
@source "../templates";
@source "../src";
npx @tailwindcss/cli -i assets/app.css -o public/build/app.css --minify

Comprobamos el resultado con una clase distinta en templates/base.html.twig, en src/Vista.php y en un HTML de public/build/. Se generaron las dos primeras, y la tercera no.

Con source(none) nada se lee salvo lo que enumeres. Las rutas son relativas a la hoja, por eso empiezan por ../: assets/ está un nivel por debajo de la raíz.

Ejercicio 5: Insignia de estado de pedido

Los pedidos de la API tienen cuatro estados: ENTREGADO, ENVIADO, PENDIENTE y CANCELADO. Un compañero ha escrito esto, y las insignias salen sin color. Explica por qué falla y corrígelo.

const colores = { ENTREGADO: 'emerald', ENVIADO: 'sky', PENDIENTE: 'amber', CANCELADO: 'rose' };

function insignia(estado) {
  const c = colores[estado];
  return `<span class="rounded-full bg-${c}-100 px-3 py-1 text-${c}-800">${estado}</span>`;
}
Ver solución

Falla porque el fichero nunca contiene bg-emerald-100 escrito entero, solo bg-${c}-100. El compilador no ejecuta el código. Las clases rounded-full, px-3 y py-1 sí funcionan, porque están completas.

La corrección es escribir las clases completas:

const clases = {
  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',
};

function insignia(estado) {
  return `<span class="rounded-full ${clases[estado]} px-3 py-1">${estado}</span>`;
}

Si los estados llegaran de fuera y no pudieras escribirlos así, la alternativa es forzar la generación:

@source inline("{bg,text}-{emerald,sky,amber,rose}-{100,800}");

Pero es peor: las clases quedan lejos del código y se generan todas, aunque no se usen.

Ejercicio 6: Diagnóstico

Un compañero te pasa tres proyectos con Tailwind 4.3. Di qué falla en cada uno y cómo arreglarlo.

Proyecto A. Compila sin errores, pero la página apenas tiene estilos.

@tailwind base;
@tailwind components;
@tailwind utilities;

Proyecto B. npm run build se detiene con un error que empieza por [postcss] It looks like you're trying to use...

export default {
  plugins: { tailwindcss: {}, autoprefixer: {} },
};

Proyecto C. Un botón debería ser semitransparente y sale opaco. No hay errores.

<button class="bg-indigo-600 bg-opacity-50 text-white">Comprar</button>
Ver solución

Proyecto A. Usa las directivas de v3. En v4, @tailwind utilities se procesa, pero el tema y Preflight no se cargan: solo se generan valores que no dependen del tema. Solución:

@import "tailwindcss";

Proyecto B. En v4 el plugin de PostCSS está en otro paquete, y autoprefixer ya no hace falta.

npm install @tailwindcss/postcss
export default {
  plugins: { '@tailwindcss/postcss': {} },
};

Proyecto C. bg-opacity-* se eliminó en v4. Como las clases desconocidas se descartan en silencio, no hay error. Usa el modificador de opacidad:

<button class="bg-indigo-600/50 text-white">Comprar</button>

Ejercicio 7: La misma configuración en la LTS

Traduce esta hoja de v4 a un proyecto en 3.4: la carpeta src/legado no debe leerse, y hay que forzar la generación de underline y de bg-emerald-100, bg-amber-100 y bg-rose-100 con su variante hover.

@import "tailwindcss";
@source not "./legado";
@source inline("underline");
@source inline("{hover:,}bg-{emerald,amber,rose}-100");
Ver solución

src/app.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

tailwind.config.js:

export default {
  content: ['./index.html', './src/**/*.{html,js}', '!./src/legado/**'],
  safelist: [
    'underline',
    { pattern: /^bg-(emerald|amber|rose)-100$/, variants: ['hover'] },
  ],
};

La comprobación en 3.4.19 generó underline, las tres clases bg-*-100, sus tres versiones hover:, y ninguna clase de src/legado/.

Fíjate en las diferencias de v3: todo va a un fichero JavaScript, el patrón es una expresión regular y las variantes se indican aparte. Las rutas de content son relativas a la carpeta desde la que se lanza la orden, y no a la hoja de estilos.

Ejercicio 8: Orden automático

Instala el plugin de Prettier en tu proyecto, y configúralo para que use tu hoja y ordene también las cadenas de clsx. Después, responde: ¿cómo quedará esta línea? ¿Qué pasará con el p-4 repetido?

const c = clsx("text-white p-4 bg-red-500 p-4");
Ver solución
npm install -D prettier prettier-plugin-tailwindcss

.prettierrc:

{
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindStylesheet": "./src/app.css",
  "tailwindFunctions": ["clsx"]
}

Resultado real:

const c = clsx("bg-red-500 p-4 text-white");

El duplicado desaparece, porque tailwindPreserveDuplicates vale false por defecto. El plugin no necesita que clsx esté instalado: reconoce el nombre de la función, sin ejecutarla.

Ejercicio 9 (propuesto): Comparar los tamaños

Compila la tarjeta de la sección 2 con la CLI de tres formas: sin opciones, con --optimize y con --minify. Mide los ficheros resultantes. Después añade diez clases nuevas y repite. ¿Cuánto crece cada uno? ¿Crece igual si repites diez veces la misma clase en elementos distintos?

Ejercicio 10 (propuesto): Una biblioteca propia

Crea en node_modules/kit-ui un paquete mínimo que exporte una cadena de clases, como en la sección 10. Impórtalo desde tu página y comprueba que el botón sale sin estilos. Después, resuélvelo de dos formas: con @source y con @source inline. ¿Qué ventajas tiene cada una si la biblioteca se actualiza?

Ejercicio 11 (propuesto): Prueba con el CDN

Crea una página con @tailwindcss/browser que genere una lista de productos con JavaScript, construyendo los colores con plantillas como `bg-${color}-500`. Comprueba que funciona. Después, pasa esa misma página a tu proyecto Vite y explica por qué deja de funcionar.


Resumen

Concepto API Idea clave
Plugin de Vite 🆕 v4.0 @tailwindcss/vite La vía recomendada si usas Vite
Hoja de entrada 🔄 v4.0 @import "tailwindcss"; Sustituye a las directivas @tailwind
Capas theme, base, components, utilities Las posteriores ganan a las anteriores, y el CSS sin capa gana a todas
CLI 🔄 v4.0 npx @tailwindcss/cli -i ... -o ... Para proyectos sin empaquetador; se enlaza la salida
Vigilancia --watch, --poll Recompila, pero no recarga el navegador
Producción npm run build, --minify --optimize no reduce el tamaño
PostCSS 🔄 v4.0 @tailwindcss/postcss Para herramientas que ya usan PostCSS
CDN 🆕 v4.0 @tailwindcss/browser@4 Solo para pruebas: genera el CSS en el navegador
LTS tailwindcss@v3-lts tailwind.config.js, content, @tailwind, autoprefixer
Detección 🔄 v4.0 Automática Lee texto plano; respeta .gitignore e ignora node_modules, CSS y binarios
Clases dinámicas Objeto de clases completas La clase debe estar escrita entera en un fichero
Añadir rutas 🔄 v4.0 @source "ruta" Rutas relativas a la hoja
Base de búsqueda 🔄 v4.0 source("ruta"), source(none) Control de la detección desde el @import
Excluir rutas 🆕 v4.1 @source not "ruta" En v3, ruta negada en content
Forzar clases 🆕 v4.1 @source inline("...") En v3, safelist
Editor bradlc.vscode-tailwindcss Autocompletado y CSS de cada clase al pasar el ratón
Orden 🔄 v4.0 prettier-plugin-tailwindcss Necesita tailwindStylesheet para conocer tu tema
Depuración ¿Todo? ¿Elemento? ¿Regla? ¿Tachada? Del problema general al concreto