Módulo 14: Personalizar el tema
Objetivos del módulo
- Entender qué es un token de diseño y por qué el tema es el centro de un proyecto con Tailwind
- Declarar el tema con
@themey traducirlo atailwind.config.jscuando trabajes en Tailwind 3.4 - Distinguir entre ampliar la escala por defecto y reemplazarla
- Conocer los espacios de nombres (
--color-*,--font-*,--text-*,--spacing,--breakpoint-*…) y saber qué clases genera cada uno - Crear una paleta de marca completa, con su contraste comprobado
- Definir fuentes y tamaños de texto con altura de línea, interletraje y peso incluidos
- Cambiar toda la densidad de una interfaz con una sola variable,
--spacing - Usar los tokens desde tu CSS con
var(),--theme(),--spacing()y--alpha(), y desde JavaScript - Saber qué variables llegan al CSS final y cuándo necesitas
@theme static - Entender el problema de cascada que resuelve
@theme inline - Reiniciar el tema entero o una parte con
initial - Construir un sistema de tokens en tres capas: primitivos, semánticos y clases
- Compartir un tema entre proyectos con un fichero, un paquete npm o un preset de Tailwind 3.4
- Diagnosticar por qué un token no genera ninguna clase
🔄 Cambia en Tailwind 4.0. Todo este módulo trata de la configuración en CSS, y en la LTS (Tailwind 3.4) el tema vive en JavaScript. Si usas la LTS: cada apartado te da el equivalente en
tailwind.config.js, y la tienda de la sección 13 se compila también en 3.4.19 con un resultado idéntico.
1. Qué es el tema
Hasta ahora has usado clases como bg-indigo-600, rounded-lg o md:flex sin preguntarte de dónde salen sus valores. No están escritos a mano uno por uno. Tailwind tiene dos partes separadas:
- Las utilidades saben qué propiedad CSS tocar:
bg-*tocabackground-color;rounded-*,border-radius. - El tema guarda los valores permitidos: qué es
indigo-600, cuánto midelg, a qué anchura empiezamd.
Cada valor con nombre del tema es un token de diseño (design token): una decisión de diseño guardada con un nombre, en lugar de repetida como número suelto por todo el código. “El azul de los botones” es una decisión; #2563eb es solo su valor actual. Si esa decisión tiene nombre, cambiarla significa editar una línea.
Por eso personalizar el tema no es un retoque final. Es lo primero que se hace en un proyecto real, porque decide qué vocabulario tendrá el equipo en su HTML.
📘 Concepto: Una clase arbitraria como
bg-[#0f766e]funciona, pero no es un token: nadie sabe si ese verde es el de la marca o uno parecido, y cambiarlo exige buscar y reemplazar. Si un valor se usa más de una o dos veces, debe pasar al tema.
Preparación del laboratorio
Sigues con el proyecto Vite del módulo 01. Todo este módulo trabaja sobre app.css, que hasta ahora tenía una sola línea:
@import "tailwindcss";
Esa línea importa, entre otras cosas, el tema por defecto. Puedes leerlo: está en node_modules/tailwindcss/theme.css y son poco más de 500 líneas de variables. Es el mejor manual que existe sobre el tema, porque te enseña los nombres reales de cada token. Ábrelo una vez; lo que verás al principio es esto:
@theme default {
--font-sans: ...;
--color-red-50: oklch(97.1% 0.013 17.38);
/* ... */
--spacing: 0.25rem;
--breakpoint-sm: 40rem;
/* ... */
}
Tu trabajo en este módulo consiste en escribir más bloques como ese.
⚠️ Importante: Vite no detecta siempre los HTML creados con el servidor en marcha. Si una clase nueva no aparece, guarda
app.csssin cambiar nada para que Tailwind vuelva a escanear el proyecto.
2. @theme frente a tailwind.config.js 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no existe
@theme: el tema es un objeto JavaScript. Si usas la LTS: escribe los tokens entheme.extenddentro detailwind.config.js; ese fichero es obligatorio en 3.4 ytailwindcss initlo crea por ti.
El primer token se declara así, debajo del @import:
@import "tailwindcss";
@theme {
--color-marca-500: #0f766e;
}
Desde ese momento existe una familia entera de clases nuevas:
<button class="bg-marca-500 px-4 py-2 text-white">Comprar</button>
<a class="text-marca-500 underline decoration-marca-500/40" href="#">Ver ficha</a>
<input class="border border-marca-500 focus:ring-2 focus:ring-marca-500">
Y esto es lo que genera Tailwind 4.3.3 para bg-marca-500:
@layer theme {
:root, :host {
--color-marca-500: #0f766e;
}
}
@layer utilities {
.bg-marca-500 {
background-color: var(--color-marca-500);
}
}
Fíjate en las dos salidas. Un token de @theme produce dos cosas: una variable CSS real, que el navegador conoce, y el permiso para que existan las clases que la usan.
Figura 1. @theme no es un :root con otro nombre: además de la variable, crea las clases.
📘 Concepto: ¿Por qué no basta con escribir
:root { --color-marca-500: #0f766e; }? Porque esa variable existiría en el navegador, pero el compilador no la consideraría parte del tema:bg-marca-500no generaría nada.@themele dice a Tailwind “esto es un token, crea sus utilidades”. Por eso tiene una sintaxis propia en lugar de ser CSS corriente.
El mismo tema en Tailwind 3.4
En 3.4 el mismo token se escribe en JavaScript:
// tailwind.config.js
export default {
content: ['./*.html'],
theme: {
extend: {
colors: {
marca: {
500: '#0f766e',
},
},
},
},
};
.bg-marca-500 {
--tw-bg-opacity: 1;
background-color: rgb(15 118 110 / var(--tw-bg-opacity, 1));
}
La diferencia no es solo de sintaxis. En 3.4 el valor se copia dentro de cada clase y no queda ninguna variable en el navegador. En 4.x la clase apunta a una variable. De ese detalle salen casi todas las ventajas de esta versión: puedes cambiar un token en tiempo de ejecución, leerlo desde JavaScript o redefinirlo en una zona de la página.
| Tarea | Tailwind 4.3 | Tailwind 3.4 |
|---|---|---|
| Dónde vive el tema | En tu CSS, con @theme | En tailwind.config.js |
| Añadir un valor | Una variable nueva | theme.extend.<clave> |
| Reemplazar una escala | --color-*: initial y tus valores | theme.<clave> sin extend |
| Rutas de contenido | Detección automática y @source | content obligatorio |
| Salida en el navegador | Variables en :root | Valores copiados en las clases |
| Cambiar un token en ejecución | Sí, es una variable | No, salvo que lo montes tú con variables |
| Leer el tema desde JavaScript | getComputedStyle | resolveConfig de tailwindcss |
| Reutilizar en otros proyectos | Importar un CSS | presets |
Ampliar o reemplazar: el error clásico de v3
Un @theme amplía el tema por defecto: tus tokens se suman a los de Tailwind, y si uno se llama igual, lo sobrescribe. En 3.4 esa misma distinción depende de un nivel de anidación, y es uno de los errores más frecuentes:
export default {
theme: {
// SIN extend: reemplaza TODOS los colores de Tailwind
colors: { marca: '#0f766e' },
},
};
Con esa configuración, de las clases bg-marca, bg-red-500, bg-white, bg-transparent, bg-current y bg-inherit, Tailwind 3.4.19 solo genera bg-marca. Las otras cinco desaparecen sin aviso, incluidas las que parecen inofensivas, porque transparent, current e inherit eran entradas más del objeto colors. La sección 12 muestra que v4 trata mejor este caso.
¿Se puede seguir usando el fichero JavaScript en v4?
Sí. @config carga un tailwind.config.js antiguo, lo que ayuda durante una migración:
@import "tailwindcss";
@config "./tailwind.config.js";
Con extend.colors.legado, por ejemplo, la clase bg-legado-500 se genera, pero con el valor copiado (background-color: #0b7285) y sin variable en :root. Es decir, los tokens que entran por @config no tienen las ventajas de @theme. La directiva se explica en el módulo 15 y la migración completa en el módulo 19.
3. Espacios de nombres 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no hay espacios de nombres: cada tipo de valor es una clave del objeto
theme. Si usas la LTS: consulta la columna “Clave en 3.4” de la tabla de esta sección;--color-marcase convierte entheme.extend.colors.marca.
Una variable de @theme no es un nombre libre. El prefijo decide qué utilidades la usan. A ese prefijo se le llama espacio de nombres (namespace), y es el concepto central de este módulo: --color-tinta y --radius-tinta no tienen nada que ver entre sí.
| Espacio de nombres | Clases que genera | Ejemplo | Clave en 3.4 |
|---|---|---|---|
--color-* | bg-*, text-*, border-*, ring-*, fill-*, from-*… | --color-marca → bg-marca | colors |
--font-* | font-* (familia) | --font-titulares → font-titulares | fontFamily |
--text-* | text-* (tamaño) | --text-titulo → text-titulo | fontSize |
--font-weight-* | font-* (peso) | --font-weight-gordo → font-gordo | fontWeight |
--tracking-* | tracking-* | --tracking-marca → tracking-marca | letterSpacing |
--leading-* | leading-* | --leading-marca → leading-marca | lineHeight |
--spacing | Escala numérica: p-4, w-13, gap-2.5… | --spacing: 0.2rem | spacing (lista de valores) |
--spacing-* | p-*, m-*, gap-*, w-*, h-*, inset-*… | --spacing-tarjeta → w-tarjeta | spacing |
--breakpoint-* | Variantes tablet:, max-tablet:, min-tablet: | --breakpoint-tablet | screens |
--container-* | max-w-*, w-* y variantes @ficha: | --container-ficha → @ficha:flex | maxWidth y containers |
--radius-* | rounded-* | --radius-tarjeta → rounded-tarjeta | borderRadius |
--shadow-* | shadow-* | --shadow-tarjeta → shadow-tarjeta | boxShadow |
--inset-shadow-* | inset-shadow-* | --inset-shadow-marca | No existe la utilidad |
--drop-shadow-* | drop-shadow-* | --drop-shadow-marca | dropShadow |
--text-shadow-* | text-shadow-* (🆕 v4.1) | --text-shadow-marca | No existe la utilidad |
--blur-* | blur-*, backdrop-blur-* | --blur-suave → blur-suave | blur |
--perspective-* | perspective-* | --perspective-marca | No existe la utilidad |
--aspect-* | aspect-* | --aspect-producto → aspect-producto | aspectRatio |
--ease-* | ease-* | --ease-rebote → ease-rebote | transitionTimingFunction |
--animate-* | animate-* | --animate-latir → animate-latir | animation y keyframes |
Todas las clases de la columna “Ejemplo” se han comprobado en 4.3.3 con un @theme que tiene un token de cada fila.
💡 Consejo: El nombre va después del prefijo y puede tener guiones:
--color-texto-suavegeneratext-texto-suave. Si la repetición te molesta, no es un error de Tailwind: es que el prefijotext-y el token dicen lo mismo. Muchos equipos prefieren--color-suavepara escribirtext-suave.
Espacios más estrechos, heredados de v3
La tabla anterior es la lista oficial, pero el compilador acepta más. Si conviertes en variables los nombres de las claves de v3, muchos funcionan:
| Funcionan en 4.3.3 | No funcionan en 4.3.3 |
|---|---|
--background-color-* (solo bg-*), --text-color-*, --border-color-*, --ring-color-* | --font-size-* (usa --text-*) |
--width-*, --min-width-*, --max-width-*, --height-*, --max-height-*, --size-* | --font-family-* (usa --font-*) |
--padding-*, --margin-*, --gap-*, --inset-*, --translate-*, --flex-basis-* | --line-height-* (usa --leading-*) |
--z-index-*, --opacity-*, --scale-*, --rotate-*, --order-*, --columns-* | --letter-spacing-* (usa --tracking-*) |
--grid-template-columns-*, --cursor-*, --list-style-type-*, --background-image-* | --border-radius-* (usa --radius-*) |
--transition-property-*, --backdrop-blur-*, --brightness-*, --stroke-width-* | --box-shadow-* (usa --shadow-*) |
--outline-color-*, --accent-color-*, --border-width-* | --aspect-ratio-*, --animation-*, --transition-timing-function-* |
Dos usos reales. El primero es limitar un color a una sola utilidad: --background-color-papel crea bg-papel sin crear text-papel. El segundo es z-index, que no tiene espacio propio en la tabla oficial: --z-index-modal: 50 genera z-modal, como viste en el módulo 07. Fíjate en la columna derecha: copiar el nombre de la propiedad CSS no funciona para tamaños de texto, fuentes, radios ni sombras, y no hay aviso.
⚠️ Importante: Una variable con un espacio de nombres desconocido no da error.
--colors-marcao--font-size-grandese aceptan en silencio y no generan ninguna clase. La sección 14 recoge cómo diagnosticarlo.
Valores por defecto: --default-*
Algunas utilidades actúan sin sufijo, y su valor sale de variables con prefijo --default-:
| Variable | Qué controla | Clave en 3.4 |
|---|---|---|
--default-font-family | Fuente de html (por defecto, --font-sans) | fontFamily.sans |
--default-mono-font-family | Fuente de code, kbd, pre, samp | fontFamily.mono |
--default-transition-duration | Duración de transition (150 ms) | transitionDuration.DEFAULT |
--default-transition-timing-function | Curva de transition | transitionTimingFunction.DEFAULT |
--default-border-width | Grosor de border | borderWidth.DEFAULT |
--default-ring-width | Grosor de ring | ringWidth.DEFAULT |
--default-ring-color | Color de ring | ringColor.DEFAULT |
Las tres últimas no aparecen en theme.css, pero el compilador las respeta. En 4.3.3, --default-border-width: 2px hace que border genere border-width: 2px, y --default-ring-width: 5px lleva ring de 1 px a 5 px. Son útiles si migras desde v3 y quieres recuperar el ring de 3 px sin tocar el HTML.
4. Colores propios 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) los colores se escriben anidados en
theme.extend.colorsy los valoresoklch()no admiten el modificador de opacidad. Si usas la LTS: usa la versión en hexadecimal de esta sección, con los niveles como claves del objetomarca.
Un color de marca suelto no basta. Necesitas fondos claros, estados hover, texto legible y bordes, y por eso las paletas de Tailwind tienen once niveles del 50 al 950. Conviene imitar esa estructura, porque así tu equipo ya sabe qué esperar de marca-100 o marca-700.
@theme {
--color-marca-50: oklch(97.5% 0.014 195);
--color-marca-100: oklch(94.5% 0.03 195);
--color-marca-200: oklch(89% 0.06 195);
--color-marca-300: oklch(81% 0.09 195);
--color-marca-400: oklch(71% 0.11 195);
--color-marca-500: oklch(60% 0.11 195);
--color-marca-600: oklch(51% 0.095 195);
--color-marca-700: oklch(43% 0.08 195);
--color-marca-800: oklch(36% 0.065 195);
--color-marca-900: oklch(30% 0.05 195);
--color-marca-950: oklch(21% 0.035 195);
--color-acento-500: oklch(70% 0.19 45);
--color-acento-600: oklch(62% 0.19 40);
--color-acento-700: oklch(52% 0.16 38);
}
¿Por qué oklch()? Lo viste en el módulo 05: sus tres números son luminosidad, intensidad y tono. Para construir una escala basta con dejar el tono fijo (195) y bajar la luminosidad paso a paso. Hacerlo a mano en hexadecimal obliga a tantear cada nivel.
<div class="flex gap-1">
<div class="h-16 flex-1 rounded-md bg-marca-50"></div>
<!-- ... del 100 al 900 ... -->
<div class="h-16 flex-1 rounded-md bg-marca-950"></div>
</div>
<button class="rounded-md bg-marca-600 px-4 py-2 font-medium text-white">bg-marca-600</button>
<button class="rounded-md border border-marca-600 px-4 py-2 font-medium text-marca-700">border-marca-600</button>
<button class="rounded-md bg-marca-600/20 px-4 py-2 font-medium text-marca-800">bg-marca-600/20</button>
<button class="rounded-md bg-acento-500 px-4 py-2 font-medium text-slate-900">bg-acento-500</button>
<span class="rounded-md px-4 py-2 font-medium ring-2 ring-marca-400 ring-offset-2">ring-marca-400</span>

Figura 2. Con declarar la escala tienes ya todas las utilidades de color, incluido el modificador /20.
El modificador de opacidad funciona solo, porque Tailwind lo convierte en color-mix():
.bg-marca-600\/20 {
background-color: color-mix(in srgb, oklch(51% 0.095 195) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-marca-600) 20%, transparent);
}
}
Comprueba el contraste antes de elegir
Que un color quede bonito no lo hace legible. Esta tabla da el contraste de cada nivel contra texto blanco, según la fórmula de WCAG, y su valor hexadecimal, que nos hará falta para la versión 3.4:
| Nivel | Hexadecimal | Contraste con blanco | Texto blanco normal (mínimo 4.5) |
|---|---|---|---|
| 400 | #31b7b7 | 2.46 | No |
| 500 | #009494 | 3.71 | No |
| 600 | #007677 | 5.42 | Sí |
| 700 | #005d5d | 7.71 | Sí, nivel AAA |
| 800 | #004748 | 10.48 | Sí, nivel AAA |
Resultado práctico: el botón principal de la marca no puede ser bg-marca-500 con texto blanco, aunque el número 500 parezca “el color de verdad”. Empieza a ser válido en el 600. Le pasó lo mismo al acento: acento-600 con letra pequeña sobre blanco daba 3.95, y por eso añadimos acento-700, que da 5.93.
Colores sin niveles y sobrescribir los de Tailwind
No todos los tokens necesitan escala. --color-marca y --color-marca-500 pueden convivir: el primero genera bg-marca y el segundo bg-marca-500.
Tampoco tienes que inventar nombres nuevos. Si declaras un nombre que ya existe, lo sobrescribes, y todos los usos existentes cambian:
@theme {
/* Recupera el rojo de la paleta de v3 */
--color-red-500: #ef4444;
}
Cuando hay dos bloques @theme, gana el último. Con --color-red-500 declarado dos veces, en :root queda solo el valor del segundo bloque.
En Tailwind 3.4
// tailwind.config.js
export default {
content: ['./*.html'],
theme: {
extend: {
colors: {
marca: {
DEFAULT: '#007677', // genera bg-marca
50: '#edfafa',
100: '#d7f4f3',
200: '#ade8e7',
300: '#75d3d3',
400: '#31b7b7',
500: '#009494',
600: '#007677',
700: '#005d5d',
800: '#004748',
900: '#023536',
950: '#011d1d',
},
acento: { 500: '#fa6e1d', 600: '#df500c', 700: '#b13c11' },
},
},
},
};
DEFAULT es la forma de v3 de decir “sin sufijo”. En v4 no hace falta: basta con declarar --color-marca aparte.
5. Fuentes y texto 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) las fuentes van en
fontFamilycomo lista y los tamaños enfontSizecomo par[tamaño, opciones]. Si usas la LTS: escribefontSize: { titulo: ['2.25rem', { lineHeight: '1.15', letterSpacing: '-0.02em', fontWeight: '700' }] }, que genera las mismas cuatro propiedades.
Familias de fuentes
@theme {
--font-titulares: Georgia, "Times New Roman", serif;
}
<h1 class="font-titulares text-4xl">Tienda EMPRESA</h1>
Esto solo crea un nombre: no descarga ninguna fuente. Si usas una tipografía que el usuario no tiene instalada, debes cargarla tú, y aquí aparecen dos reglas.
Con un fichero propio, @font-face va fuera de @theme, porque @theme solo admite variables y @keyframes:
@import "tailwindcss";
@font-face {
font-family: "Empresa";
src: url("/fuentes/empresa.woff2") format("woff2");
font-display: swap;
}
@theme {
--font-empresa: "Empresa", sans-serif;
}
Con Google Fonts, el @import url() va antes del de Tailwind:
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap");
@import "tailwindcss";
@theme {
--font-titulares: "Poppins", sans-serif;
}
Si lo escribes después de @import "tailwindcss", el navegador lo ignora, porque un @import debe preceder a las demás reglas. Vite lo avisa, aunque es fácil no verlo, durante build:
Found 1 warning while optimizing generated CSS:
│ @import url("https://fonts.googleapis.com/css2?family=Poppins");
┆ ^-- @import rules must precede all rules aside from @charset and @layer statements
Para cambiar la fuente de toda la página sin poner una clase en cada elemento, sobrescribe --font-sans, o bien apunta --default-font-family a otro token. Preflight aplica --default-font-family a html, y ese valor vale --font-sans salvo que lo cambies:
@theme {
--default-font-family: var(--font-titulares);
}
También puedes adjuntar ajustes de fuente al token con un doble guion, y font-tabla los aplica todos:
@theme {
--font-tabla: "Inter", sans-serif;
--font-tabla--font-feature-settings: "tnum";
--font-tabla--font-variation-settings: "wght" 450;
}
Tamaños con sus compañeros
Un tamaño de texto casi nunca viaja solo. Un título grande necesita menos altura de línea e interletraje algo negativo. Por eso --text-* admite subvalores con doble guion, y una sola clase aplica los cuatro:
@theme {
--text-titulo: 2.25rem;
--text-titulo--line-height: 1.15;
--text-titulo--letter-spacing: -0.02em;
--text-titulo--font-weight: 700;
--text-precio: 1.5rem;
--text-precio--line-height: 1;
--text-precio--font-weight: 800;
}
.text-titulo {
font-size: var(--text-titulo);
line-height: var(--tw-leading, var(--text-titulo--line-height));
letter-spacing: var(--tw-tracking, var(--text-titulo--letter-spacing));
font-weight: var(--tw-font-weight, var(--text-titulo--font-weight));
}
Mira bien esas tres últimas líneas. Las clases leading-*, tracking-* y font-* rellenan --tw-leading, --tw-tracking y --tw-font-weight. Como var() usa su segundo argumento solo si el primero está vacío, los subvalores son valores por defecto que puedes sobrescribir con una clase, sin importar el orden del CSS:
<h1 class="font-titulares text-titulo text-marca-900">Catálogo de otoño para tu oficina</h1>
<p class="text-precio text-marca-700">349,00 €</p>
<p class="text-precio font-light leading-loose text-marca-700">349,00 €</p>

Figura 3. text-titulo trae peso, altura de línea e interletraje. Abajo a la derecha, font-light leading-loose sobrescribe esos valores por defecto.
Si un --text-* no lleva --line-height, la clase solo genera font-size y la altura de línea se hereda. También puedes pasarla al usarla: text-etiqueta/7 da line-height: calc(var(--spacing) * 7).
6. Espaciado, medidas y puntos de ruptura 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) la escala de espaciado es una lista de valores fijos en
rem, y no existe una variable base. Si usas la LTS: regenera la escala entheme.spacingcon un bucle, como en el ejemplo de esta sección; el cambio solo se aplica al compilar, no a una zona de la página.
Una sola variable para toda la densidad
Toda la escala numérica se calcula a partir de una variable. p-4 no vale 1rem; vale cuatro veces --spacing:
.p-4 {
padding: calc(var(--spacing) * 4);
}
Por tanto, si cambias --spacing, cambia todo lo que usa la escala a la vez: rellenos, márgenes, huecos, anchuras y alturas.
@theme {
--spacing: 0.2rem; /* interfaz más compacta */
}
.p-13 { padding: calc(var(--spacing) * 13); }
.w-96 { width: calc(var(--spacing) * 96); }
.-m-2 { margin: calc(var(--spacing) * -2); }
.m-0 { margin: 0px; }
Y como es una variable CSS, también puedes redefinirla en un elemento concreto. Este HTML es el mismo tres veces; solo cambia el style:
<article style="--spacing: 0.2rem" class="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
<h3 class="font-semibold text-slate-900">Portátil Ligero 14</h3>
<p class="mt-2 text-sm text-slate-600">Ideal para teletrabajo y clases.</p>
<div class="mt-4 flex items-center gap-2">
<button class="rounded-md bg-marca-600 px-3 py-1.5 text-sm text-white">Comprar</button>
<button class="rounded-md border border-slate-300 px-3 py-1.5 text-sm">Ver ficha</button>
</div>
</article>

Figura 4. Relleno, márgenes, huecos y padding de los botones cambian a la vez. El tamaño de letra no, porque no depende de --spacing.
Esta técnica sirve para un modo “compacto” en tablas de administración o paneles densos, sin duplicar clases. Solo afecta a la escala numérica: p-px sigue siendo 1 px y text-sm sigue igual.
En 3.4 lo más parecido es generar la escala con un bucle:
const pasos = [0, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24];
export default {
theme: {
spacing: {
px: '1px',
...Object.fromEntries(pasos.map((n) => [n, `${(n * 2) / 10}rem`])),
},
},
};
Ojo: al escribirlo sin extend, solo existen las claves listadas. Con esa lista, p-4 da 0.8rem, pero p-7 deja de existir.
Medidas con nombre
Además de la escala, puedes crear medidas sueltas. Fíjate en que --spacing-* y --container-* alimentan utilidades de tamaño:
@theme {
--spacing-tarjeta: 22rem; /* p-tarjeta, w-tarjeta, gap-tarjeta... */
--container-ficha: 22rem; /* max-w-ficha, w-ficha, @ficha: */
}
⚠️ Importante: Si un nombre existe en los dos espacios,
max-w-tarjetausa el de--spacing-*sin avisar, como se vio en el módulo 03. Evita repetir el nombre.
Radios, sombras y otros valores
@theme {
--radius-tarjeta: 1.25rem;
--shadow-tarjeta: 0 2px 4px rgb(0 0 0 / 0.06), 0 10px 24px -6px rgb(0 0 0 / 0.18);
--ease-rebote: cubic-bezier(0.3, 1.6, 0.6, 1);
--aspect-producto: 4 / 3;
}
<div class="rounded-lg bg-white p-4 shadow-md">...</div>
<div class="rounded-tarjeta bg-white p-4 shadow-tarjeta">...</div>
<div class="max-w-ficha rounded-tarjeta bg-marca-50 p-4 shadow-tarjeta">...</div>

Figura 5. Los tokens de forma se combinan con los demás como cualquier otra clase.
Una ventaja de --shadow-*: Tailwind separa el color de la sombra. Con --shadow-tarjeta: 0 2px 4px rgb(0 0 0 / 0.1), la clase genera 0 2px 4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), así que shadow-red-500 puede teñirla. Si tu sombra tiene varias capas, cada una recibe el mismo tratamiento.
Las animaciones son el único caso en el que @theme admite algo que no es una variable. Lo viste en el módulo 13: @keyframes puede ir dentro del bloque, y solo sale al CSS si usas la animación.
Puntos de ruptura propios
@theme {
--breakpoint-tablet: 50rem;
}
<div class="grid gap-3 tablet:grid-cols-3">
<div class="rounded-tarjeta bg-marca-100 p-4 font-medium text-marca-900">Monitor 27</div>
<div class="rounded-tarjeta bg-marca-100 p-4 font-medium text-marca-900">Teclado mecánico</div>
<div class="rounded-tarjeta bg-marca-100 p-4 font-medium text-marca-900">Ratón ergonómico</div>
</div>

Figura 6. A 700 px no se cumple tablet:, porque 50rem son 800 px.

Figura 7. A 860 px se aplica tablet:grid-cols-3.
tablet:, max-tablet: y min-tablet: aparecen sin hacer nada más, y Tailwind coloca la media en su sitio según el valor. Con --breakpoint-xxl: 90rem añadido a los demás, el CSS sale ordenado así, con 90rem antes de los 96rem de 2xl:
(width >= 40rem) sm
(width >= 64rem) lg
(width >= 90rem) xxl
(width >= 96rem) 2xl
⚠️ Importante: Declara los puntos de ruptura en
rem, como los de Tailwind. En el módulo 10 viste que v4 ordena los valores enpxantes que todos los puntos enrem, sea cual sea su medida real. Con unidades mezcladas el orden puede salir mal sin ningún aviso.
En 3.4, ampliar es igual de sencillo (theme.extend.screens: { tablet: '50rem' }), pero el orden depende de la declaración. Un xs: '30rem' en extend.screens se coloca después de md, de modo que xs:flex sobrescribe a md: en lugar de cederle el paso. Y además desactiva max-* y min-*, con este aviso:
warn - The `min-*` and `max-*` variants are not supported with a `screens` configuration containing mixed units.
Para añadir un punto por debajo de sm en 3.4, redeclara theme.screens entero, en orden y con una sola unidad. Si solo cambias theme.screens: { tablet: '50rem', escritorio: '64rem' }, desaparecen md: y compañía, pero max-tablet: funciona.
7. Usar los tokens fuera de las clases 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) los tokens no llegan al navegador como variables, y
--theme(),--spacing()y--alpha()no existen. Si usas la LTS: en tu CSS usatheme(colors.marca.500 / 50%)ytheme(spacing.4), y en JavaScriptresolveConfigdetailwindcss/resolveConfig, que mete el tema entero en tu paquete.
En tu CSS
Como cada token es una variable, la forma natural de usarlo es var(). Es lo que escribirías en una hoja propia o en un style:
.grafico-barra {
fill: var(--color-marca-500);
border-radius: var(--radius-tarjeta);
}
<div style="border-color: var(--color-marca-500)">...</div>
Tailwind añade además tres funciones que se resuelven al compilar:
@theme {
--color-marca-500: #e8590c;
}
.aviso {
/* Con opacidad, sin tener que escribir color-mix a mano */
background: --theme(--color-marca-500 / 50%);
/* Múltiplo de la escala */
margin: --spacing(4);
/* Opacidad sobre cualquier color */
border-color: --alpha(var(--color-marca-500) / 30%);
/* El valor literal, sin var() */
outline-color: --theme(--color-marca-500 inline);
}
Este es el CSS real que genera 4.3.3. Observa el @supports que añade para navegadores sin oklab:
.aviso {
background: color-mix(in srgb, #e8590c 50%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background: color-mix(in oklab, var(--color-marca-500) 50%, transparent);
}
margin: calc(var(--spacing) * 4);
border-color: color-mix(in srgb, #e8590c 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-marca-500) 30%, transparent);
}
outline-color: #e8590c;
}
Las dos funciones de abajo son especialmente útiles dentro de valores arbitrarios:
<main class="h-[calc(100vh---spacing(4))]">...</main>
height: calc(100vh - calc(var(--spacing) * 4));
--theme() es además más seguro que var(): si escribes mal el nombre, falla la compilación, mientras que un var() mal escrito pasa desapercibido hasta que lo ves en pantalla.
Could not resolve value for theme function: `theme(--color-noexiste-500)`.
💡 Consejo: La antigua función
theme(spacing.4)de v3 sigue funcionando en 4.3.3, por compatibilidad, y da el valor literal (1rem). En código nuevo usa--spacing(4), que respeta los cambios de--spacingen una zona de la página.
En JavaScript
Una variable CSS se puede leer desde JavaScript con getComputedStyle. Es útil cuando el color acaba en un sitio al que CSS no llega, como un <canvas>:
<canvas id="grafico" width="520" height="200"></canvas>
const estilos = getComputedStyle(document.documentElement);
const token = (nombre) => estilos.getPropertyValue(nombre).trim();
const ventas = [120, 185, 150, 240];
const colores = ['--color-marca-300', '--color-marca-500', '--color-marca-700', '--color-acento-500'];
const lienzo = document.querySelector('#grafico');
const ctx = lienzo.getContext('2d');
ventas.forEach((valor, i) => {
ctx.fillStyle = token(colores[i]);
ctx.fillRect(30 + i * 120, 190 - valor * 0.7, 90, valor * 0.7);
ctx.fillStyle = token('--color-marca-950');
ctx.font = '14px sans-serif';
ctx.fillText('T' + (i + 1) + ': ' + valor, 55 + i * 120, 182 - valor * 0.7);
});

Figura 8. El <canvas> no entiende clases, pero sí acepta los valores de las variables del tema. Debajo, el valor leído con getPropertyValue.
Si cambias un color en el tema, el gráfico lo recoge sin tocar el JavaScript. Hay un detalle: getPropertyValue devuelve el texto tal como se declaró (oklch(60% 0.11 195)), sin convertirlo. Eso le sirve a <canvas> en un navegador actual, pero no si una biblioteca espera hexadecimal.
Aquí los nombres de las variables están escritos enteros en el fichero, y eso importa más de lo que parece. La siguiente sección explica por qué, y qué hacer si no lo están.
8. Qué llega al CSS: @theme static 🆕 v4.0
🆕 Desde Tailwind 4.0. No existe en la LTS (Tailwind 3.4). Si usas la LTS: expón a mano los tokens que necesites, con
@layer base { :root { --color-marca-500: theme(colors.marca.500); } }.
Si el tema por defecto tiene más de 500 líneas, ¿por qué dist no las lleva todas? Porque Tailwind elimina las variables que no se usan, igual que hace con las clases. Con este tema y solo la clase bg-x:
@theme {
--color-x: red;
--color-y: blue;
}
@layer theme {
:root, :host {
--color-x: red;
}
}
--color-y no sale. Una variable se considera “usada” en tres casos:
- La usa una clase presente en tus ficheros, como
bg-x. - Aparece en tu propio CSS, como
color: var(--color-y). - Su nombre aparece escrito entero en algún fichero fuente.
El tercer caso es una ayuda interesante y tiene una trampa. Míralo con varios tokens --color-rojizo-* y este JavaScript:
const estilos = getComputedStyle(document.documentElement);
// La clase bg-rojizo-100 aparece en el HTML
estilos.getPropertyValue('--color-rojizo-100'); // '#fde2e2'
// Sin clase, pero el nombre está escrito entero en este fichero
estilos.getPropertyValue('--color-rojizo-200'); // '#fbbcbc'
// El nombre se construye al ejecutar: el escáner nunca lo ve
const n = 300;
estilos.getPropertyValue(`--color-rojizo-${n}`); // ''
El tercer resultado es una cadena vacía, sin error ni aviso. Es el mismo motivo por el que bg-${color}-500 falla, que se trata a fondo en el módulo 18: el escáner lee texto, no ejecuta tu código.
La solución es pedir que salgan todas:
@theme static {
--color-rojizo-100: #fde2e2;
--color-rojizo-200: #fbbcbc;
--color-rojizo-300: #f79999;
}
Con static los tres valores llegan siempre al CSS, se usen o no.
¿Y si quieres todo el tema por defecto en :root, por ejemplo para que un tercero lo personalice? Se pide en el import:
@import "tailwindcss" theme(static);
También puedes aplicar un modo a un fichero entero, sin retocar su contenido:
@import "./tema.css" theme(static);
💡 Consejo: No uses
staticpor costumbre. Cada variable son bytes que van a todos los usuarios. Resérvalo para los tokens que se leen en ejecución con nombres calculados. El tamaño final se analiza en el módulo 20.
9. @theme inline: el problema de la cascada 🆕 v4.0
🆕 Desde Tailwind 4.0. No existe en la LTS (Tailwind 3.4), y no hace falta: allí los valores siempre se copian en la clase. Si usas la LTS: escribe
colors: { lienzo: 'var(--superficie)' }; si necesitas opacidad, usargb(var(--superficie) / <alpha-value>)con la variable en canales RGB.
Esta es la parte más sutil del módulo. Merece la pena verla fallar antes de explicarla.
Quieres un token cuyo valor dependa de otra variable, para que una zona de la página tenga otro aspecto:
@import "tailwindcss";
:root {
--superficie: #ffffff;
}
@theme {
--color-caja: var(--superficie);
}
.zona-oscura {
--superficie: #1e293b;
}
<div class="zona-oscura">
<div class="bg-caja">...</div>
</div>
Parece correcto, y bg-caja se genera. Pero la tarjeta sigue blanca.

Figura 9. A la izquierda, el fallo. Las otras dos tarjetas muestran las dos formas de resolverlo.
La causa no es Tailwind: es cómo funcionan las variables CSS. El CSS generado es este:
:root {
--color-caja: var(--superficie);
}
.bg-caja {
background-color: var(--color-caja);
}
Una variable que contiene var() se resuelve en el elemento donde se declara. --color-caja está en :root, así que el navegador sustituye var(--superficie) por el valor que tiene --superficie en :root, que es blanco. Los descendientes heredan ese resultado ya calculado. Para cuando llegas a .zona-oscura da igual lo que valga --superficie, porque nadie vuelve a mirarla.
Figura 10. La variable intermedia de :root congela el valor. Quitándola, el var() se resuelve en la propia tarjeta.
@theme inline elimina ese paso intermedio. El valor del token se copia dentro de la clase y no se emite ninguna variable en :root:
@theme inline {
--color-lienzo: var(--superficie);
}
.bg-lienzo {
background-color: var(--superficie);
}
.bg-lienzo\/50 {
background-color: var(--superficie);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--superficie) 50%, transparent);
}
}
Ahora var(--superficie) se evalúa en cada elemento que usa la clase, y ahí sí se ve el valor de su zona.
Cuándo usar inline y cuándo no
Con un valor literal, inline cambia poco, pero te quita la variable: --color-z: #123 en inline da color: #123, y ya no podrás leer --color-z desde tu CSS ni sobrescribirla. La regla práctica:
| Situación | Modo |
|---|---|
Token con un valor fijo (#0f766e, 1.25rem) | @theme |
| Token que apunta a una variable que cambia por zona o tema | @theme inline |
| Token que apunta a otro token fijo del tema | @theme |
La tercera fila también funciona: --color-primario: var(--color-marca) en un @theme normal saca las dos variables y funciona bien, siempre que no redefinas --color-marca por debajo de :root.
La otra solución: sobrescribir el token
La tercera tarjeta de la figura usa @theme normal, sin inline. En lugar de cambiar la variable de origen, redefine el propio token en la zona:
.zona-directa {
--color-caja: #1e293b;
}
Funciona porque .bg-caja lee var(--color-caja) y esa variable sí se resuelve en la zona. Es la técnica más directa, y es la que usa el módulo 12 para los temas. Elige una u otra según cómo quieras organizar los tokens; las dos son correctas.
⚠️ Importante:
@themeno se puede meter en un@mediapara crear variantes. El compilador 4.3.3 no da error: extrae el bloque y coloca las variables en:root, sin la condición. Con@media (prefers-color-scheme: dark) { @theme { --color-fondo: #000; } }, ese#000se aplica siempre, también en modo claro. Lo mismo pasa si lo metes dentro de un selector. Declara@themesiempre en el nivel superior del fichero. Para valores por tema, redefine las variables en una regla normal.
10. Los otros modos: reference y default 🆕 v4.0
🆕 Desde Tailwind 4.0. No existe en la LTS (Tailwind 3.4). Si usas la LTS: el equivalente de
defaultes un preset, porque elthemedel proyecto ya tiene prioridad sobre él.referenceno tiene equivalente: en 3.4 los valores se copian en las clases y nunca se generan variables.
@theme admite dos opciones más que aparecen sobre todo cuando escribes temas para otros. Las dos se usan en el propio fichero de Tailwind, y las opciones se pueden combinar: el tema por defecto se declara con @theme default y los alias obsoletos del final de theme.css, con @theme default inline reference.
@theme reference
No emite variables, pero la clase conserva el nombre y lleva el valor como alternativa:
@theme reference {
--color-x: red;
}
.bg-x {
background-color: var(--color-x, red);
}
Sirve cuando otra hoja ya declara esa variable en :root, y así no la duplicas.
@theme default
Un valor default cede ante cualquier valor normal, sin importar el orden:
@theme { --color-marca: red; }
@theme default { --color-marca: blue; }
Compilado en los dos órdenes, el resultado es el mismo: --color-marca: red. Es la herramienta ideal para un tema compartido, que ofrece valores de partida y deja la última palabra al proyecto. Lo usaremos en la sección 15.
Este resumen de los cuatro modos te servirá de consulta:
Figura 11. Los modos cambian la salida, no los nombres de las clases.
11. El orden importa: dónde colocar @theme
Por lo general el orden es el natural, pero conviene conocer tres reglas:
- Dos bloques normales: gana el último.
- Un bloque
default: cede siempre ante uno normal. @themeantes de@import "tailwindcss": tu valor sigue ganando, porque el tema de Tailwind esdefault. Pero hay un efecto secundario.
Mira qué pasa con un tema.css que redefine --color-red-500, importado antes que Tailwind:
@import "./tema.css"; /* ANTES */
@import "tailwindcss";
/* Salida: las variables quedan fuera de la capa theme */
:root, :host {
--color-red-500: #123456;
--color-marca: #654321;
/* ... */
}
@layer theme;
Una regla fuera de capa gana a cualquier capa, de modo que después no podrás sobrescribir esas variables en :root desde @layer base. Con el import después de Tailwind, las variables quedan dentro de @layer theme, que es lo esperado:
@import "tailwindcss";
@import "./tema.css"; /* DESPUÉS: la forma correcta */
12. Reiniciar el tema 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no existe
initial: una escala se reinicia escribiéndola enthemesinextend, y todo el tema conpresets: []. Si usas la LTS: para quitar los colores usatheme: { colors: { transparent: 'transparent', current: 'currentColor', marca: '#0f766e' } }, incluyendo esas dos entradas a mano.
A veces no quieres ampliar la paleta, sino cerrarla. Si bg-red-500 existe, alguien acabará usándolo aunque no sea un color de la marca, y con el tiempo la interfaz se llena de tonos parecidos. Un sistema de diseño cerrado solo permite lo aprobado.
El valor especial initial borra tokens. Con un asterisco borra un espacio de nombres entero:
@import "tailwindcss";
@theme {
--color-*: initial;
--color-marca: #0f766e;
--color-papel: #fbfaf7;
--color-tinta: #1f2933;
}
<button class="rounded-md bg-marca px-4 py-2 text-papel">bg-marca</button>
<button class="rounded-md bg-red-500 px-4 py-2 text-white">bg-red-500 text-white</button>
<button class="rounded-md bg-slate-200 px-4 py-2">bg-slate-200</button>
<button class="rounded-md bg-transparent px-4 py-2 ring-1 ring-current">bg-transparent ring-current</button>
<button class="rounded-md bg-marca/15 px-4 py-2 text-marca">bg-marca/15</button>

Figura 12. Las clases de la paleta eliminada no generan CSS. transparent, current y ring-* siguen funcionando.
Compara el resultado con el de v3: bg-white y bg-black desaparecen, porque son colores de la paleta, pero bg-transparent, bg-current y bg-inherit sobreviven, porque en v4 no son tokens. Si quieres conservar el blanco, añádelo: --color-white: #fff.
Los niveles de reinicio
| Sintaxis | Qué borra | Comprobado |
|---|---|---|
--color-red-500: initial | Un solo token | bg-red-500 no se genera; bg-red-400 sí |
--color-*: initial | Toda la paleta | bg-red-500, bg-white no se generan |
--shadow-*: initial | Todas las sombras | shadow-md no; shadow-none sí |
--breakpoint-*: initial | Todos los puntos | md: no se genera; los tuyos sí |
--breakpoint-2xl: initial | Un punto concreto | 2xl: no se genera |
--spacing: initial | La escala numérica | p-4 no; p-px y p-tarjeta sí |
--*: initial | Todo el tema | Ver abajo |
Empezar de cero: --*: initial
@theme {
--*: initial;
--spacing: 0.25rem;
--color-marca: #0f766e;
}
Esto vacía todo. De una lista de prueba, estas clases se generan y estas no:
| Se generan (no dependen del tema) | No se generan |
|---|---|
flex, w-full, w-1/2, m-px, p-[3px] | text-lg, rounded-lg, shadow-md, font-bold, md:flex |
transition; p-4, si declaras --spacing | p-4, si no declaras --spacing |
Fíjate en un efecto colateral: al borrar también --default-transition-duration, la clase transition queda con transition-duration: var(--tw-duration, 0s). Es decir, deja de animar hasta que declares esa variable o añadas duration-*. La fuente base no se rompe, porque Preflight tiene su propia lista alternativa.
Tendrás que declarar a mano todo lo que necesites. Se llega al mismo punto sin borrar nada, si no importas el tema:
@layer theme, base, components, utilities;
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css" layer(utilities);
@theme {
--spacing: 0.25rem;
--color-marca: #0f766e;
}
En 3.4, el equivalente es presets: [], que anula toda la configuración por defecto. Con él no se generan p-4 ni text-lg, mientras que flex sí. Se recomienda poco, porque hay que redefinir decenas de escalas.
💡 Consejo:
--color-*: initiales razonable;--*: initialcasi nunca lo es. Cerrar la paleta da orden con muy poco esfuerzo. Cerrarlo todo te obliga a mantener tu propia escala de tamaños, pesos y sombras, y la de Tailwind ya es buena.
13. Una marca propia completa 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) el reparto en capas se monta con un preset y variables con los canales RGB separados. Si usas la LTS: define los colores como
rgb(var(--primario) / <alpha-value>)y las variables como--primario: 0 93 93; el código completo está al final de esta sección.
Vamos a juntar todo en la tienda de EMPRESA. Partimos de una versión con los valores por defecto, la que sale al usar solo clases de Tailwind:
<article class="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
<span class="text-xs font-medium text-indigo-700">Informática</span>
<h2 class="mt-1 font-semibold text-slate-900">Portátil Ligero 14</h2>
<p class="mt-3 text-xl font-bold text-slate-900">749,00 €</p>
<p class="text-sm text-slate-500">En stock</p>
<button class="mt-4 w-full rounded-md bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-700">
Añadir al carrito
</button>
</article>

Figura 13. Correcta, pero con el aspecto que tienen miles de webs hechas con Tailwind.
Esa versión tiene otro problema más grave que el aspecto: el HTML está lleno de decisiones de color. Si mañana quieres modo oscuro, cada clase necesita su dark:. Si quieres una segunda marca, hay que tocarlo todo.
Tokens en tres capas
La solución habitual en sistemas de diseño es separar los tokens en capas:
Figura 14. Los primitivos guardan valores, los semánticos les dan un papel y el HTML solo nombra papeles.
- Los primitivos son los valores crudos:
--color-marca-700. Dicen qué color es, no para qué sirve. - Los semánticos son papeles:
--primario,--fondo,--linea. Dicen para qué sirve, y su valor cambia según el tema. - Las clases se generan desde los semánticos:
bg-primario,text-texto. Es lo que escribe el equipo en el HTML.
Aquí @theme inline es imprescindible: --color-primario debe apuntar a --primario y ese valor debe poder cambiar por zona, exactamente el problema de la sección 9.
El CSS completo
@import "tailwindcss";
/* 1. Variante oscura controlada por atributo (módulo 12) */
@custom-variant dark (&:where([data-tema="oscuro"], [data-tema="oscuro"] *));
/* 2. Tokens primitivos: los valores crudos de la marca */
@theme {
--color-marca-50: oklch(97.5% 0.014 195);
--color-marca-100: oklch(94.5% 0.03 195);
--color-marca-200: oklch(89% 0.06 195);
--color-marca-300: oklch(81% 0.09 195);
--color-marca-400: oklch(71% 0.11 195);
--color-marca-500: oklch(60% 0.11 195);
--color-marca-600: oklch(51% 0.095 195);
--color-marca-700: oklch(43% 0.08 195);
--color-marca-800: oklch(36% 0.065 195);
--color-marca-900: oklch(30% 0.05 195);
--color-marca-950: oklch(21% 0.035 195);
--color-acento-500: oklch(70% 0.19 45);
--color-acento-600: oklch(62% 0.19 40);
--color-acento-700: oklch(52% 0.16 38);
--font-titulares: Georgia, "Times New Roman", serif;
--text-titulo: 2.25rem;
--text-titulo--line-height: 1.15;
--text-titulo--letter-spacing: -0.02em;
--text-titulo--font-weight: 700;
--text-precio: 1.5rem;
--text-precio--line-height: 1;
--text-precio--font-weight: 800;
--radius-tarjeta: 1.25rem;
--shadow-tarjeta: 0 2px 4px rgb(0 0 0 / 0.06), 0 10px 24px -6px rgb(0 0 0 / 0.18);
}
/* 3. Tokens semánticos: la clase apunta a una variable que cambia de valor */
@theme inline {
--color-fondo: var(--fondo);
--color-superficie: var(--superficie);
--color-texto: var(--texto);
--color-suave: var(--suave);
--color-linea: var(--linea);
--color-primario: var(--primario);
--color-primario-hover: var(--primario-hover);
--color-sobre-primario: var(--sobre-primario);
--color-etiqueta: var(--etiqueta);
}
/* 4. Valores de cada tema */
@layer base {
:root {
--fondo: var(--color-marca-50);
--superficie: #ffffff;
--texto: var(--color-marca-950);
--suave: var(--color-marca-800);
--linea: var(--color-marca-200);
--primario: var(--color-marca-700);
--primario-hover: var(--color-marca-800);
--sobre-primario: #ffffff;
--etiqueta: var(--color-acento-700);
}
[data-tema="oscuro"] {
--fondo: var(--color-marca-950);
--superficie: var(--color-marca-900);
--texto: var(--color-marca-50);
--suave: var(--color-marca-200);
--linea: var(--color-marca-800);
--primario: var(--color-marca-300);
--primario-hover: var(--color-marca-200);
--sobre-primario: var(--color-marca-950);
--etiqueta: var(--color-acento-500);
}
/* Segunda marca sobre el mismo HTML */
[data-marca="outlet"] {
--primario: var(--color-acento-500);
--primario-hover: var(--color-acento-600);
--sobre-primario: var(--color-marca-950);
--etiqueta: var(--color-marca-700);
}
}
Fíjate en el detalle del @layer base: las reglas de :root usan var(--color-marca-50) y similares. Como viste en la sección 8, usarlas en tu CSS basta para que Tailwind las incluya, aunque ninguna clase las pida.
El HTML
El HTML ya no menciona ningún color concreto:
<body class="bg-fondo">
<h1 class="font-titulares text-titulo text-texto">Tienda EMPRESA</h1>
<p class="mt-1 text-suave">Equipa tu oficina con lo esencial.</p>
<article class="rounded-tarjeta border border-linea bg-superficie p-4 shadow-tarjeta">
<span class="text-xs font-medium text-etiqueta">Informática</span>
<h2 class="mt-1 font-titulares font-semibold text-texto">Portátil Ligero 14</h2>
<p class="mt-3 text-precio text-texto">749,00 €</p>
<p class="text-sm text-suave">En stock</p>
<button class="mt-4 w-full rounded-tarjeta bg-primario px-3 py-2 text-sm font-medium text-sobre-primario hover:bg-primario-hover">
Añadir al carrito
</button>
</article>
</body>

Figura 15. La tienda con la marca de EMPRESA.
Para el modo oscuro y para la segunda marca no hay que tocar ni una clase. Basta con un atributo en <html>:
<html data-tema="oscuro">

Figura 16. data-tema="oscuro". No hay ni un dark: en el HTML.
<html data-marca="outlet">

Figura 17. data-marca="outlet": mismo HTML, otra marca. Solo cambian cuatro variables.
📘 Concepto: La variante
dark:del módulo 12 y los tokens semánticos no se excluyen. Lo normal es usar tokens para lo repetitivo (fondos, textos, bordes) ydark:para las excepciones puntuales de un componente.
Comprueba también el contraste de los pares semánticos, porque cada tema es una combinación nueva. Con la misma fórmula: sobre-primario frente a primario da 7.71 en claro y 10.07 en oscuro. El naranja del outlet con texto marca-950 da 6.12. Con acento-600 daba 4.44, por debajo del mínimo, y por eso el outlet usa acento-500. etiqueta en oscuro sobre superficie da 4.66. Un sistema de tokens centraliza estos cálculos: los haces una vez por tema, no una vez por componente.
La misma tienda en Tailwind 3.4
En 3.4 se puede llegar al mismo resultado. El HTML es exactamente el mismo; solo cambia cómo se escribe el tema. Colores en hexadecimal, y los semánticos con canales RGB para no perder el modificador de opacidad:
// preset-empresa.cjs
const variable = (nombre) => `rgb(var(--${nombre}) / <alpha-value>)`;
module.exports = {
theme: {
extend: {
colors: {
marca: {
50: '#edfafa', 100: '#d7f4f3', 200: '#ade8e7', 300: '#75d3d3',
400: '#31b7b7', 500: '#009494', 600: '#007677', 700: '#005d5d',
800: '#004748', 900: '#023536', 950: '#011d1d',
},
acento: { 500: '#fa6e1d', 600: '#df500c', 700: '#b13c11' },
fondo: variable('fondo'),
superficie: variable('superficie'),
texto: variable('texto'),
suave: variable('suave'),
linea: variable('linea'),
primario: {
DEFAULT: variable('primario'),
hover: variable('primario-hover'),
},
'sobre-primario': variable('sobre-primario'),
etiqueta: variable('etiqueta'),
},
fontFamily: {
titulares: ['Georgia', '"Times New Roman"', 'serif'],
},
fontSize: {
titulo: ['2.25rem', { lineHeight: '1.15', letterSpacing: '-0.02em', fontWeight: '700' }],
precio: ['1.5rem', { lineHeight: '1', fontWeight: '800' }],
},
borderRadius: { tarjeta: '1.25rem' },
boxShadow: {
tarjeta: '0 2px 4px rgb(0 0 0 / 0.06), 0 10px 24px -6px rgb(0 0 0 / 0.18)',
},
},
},
};
/* app.css en 3.4 */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--fondo: 237 250 250;
--superficie: 255 255 255;
--texto: 1 29 29;
--suave: 0 71 72;
--linea: 173 232 231;
--primario: 0 93 93;
--primario-hover: 0 71 72;
--sobre-primario: 255 255 255;
--etiqueta: 177 60 17;
}
[data-tema='oscuro'] {
--fondo: 1 29 29;
--superficie: 2 53 54;
--texto: 237 250 250;
--suave: 173 232 231;
--linea: 0 71 72;
--primario: 117 211 211;
--primario-hover: 173 232 231;
--sobre-primario: 1 29 29;
--etiqueta: 250 110 29;
}
}

Figura 18. La misma tienda compilada con Tailwind 3.4.19. El resultado es igual al de la figura 15.
¿Por qué esos números separados? Porque bg-primario/50 en 3.4 necesita meter la opacidad dentro del color. Estas son las tres formas posibles, y lo que genera cada una:
| Definición en 3.4 | bg-fondo | bg-fondo/50 |
|---|---|---|
'var(--fondo)' | background-color: var(--fondo) | No se genera, en silencio |
'rgb(var(--fondo) / <alpha-value>)' | Correcto, con --fondo: 237 250 250 | rgb(var(--fondo) / 0.5) |
'color-mix(in srgb, var(--fondo) calc(<alpha-value> * 100%), transparent)' | Correcto, con cualquier color | color-mix(in srgb, var(--fondo) calc(0.5 * 100%), transparent) |
La tercera deja escribir las variables con cualquier color, pero exige navegadores con color-mix(): los mismos que pide v4. Si estás en 3.4 precisamente por los navegadores antiguos, usa la segunda.
14. Por qué no aparece mi clase
Un tema mal escrito falla en silencio: no hay error, solo una clase que no hace nada. Estos son cinco errores típicos, y a continuación lo que hace el compilador con cada uno:
@theme {
--colors-exito: #16a34a; /* Plural: espacio de nombres desconocido */
--font-size-grande: 3rem; /* Es el nombre de la propiedad CSS */
--color-Peligro: #dc2626; /* Con mayúscula */
--shadows-suave: 0 1px red; /* Plural */
--spacing-tarjeta: 22; /* Sin unidad */
}
| Clase usada | ¿Se genera? | Causa | Arreglo |
|---|---|---|---|
bg-exito | No | --colors-* no existe | --color-exito |
text-grande | No | El espacio de los tamaños es --text-* | --text-grande |
bg-peligro | No | Los nombres distinguen mayúsculas; se genera bg-Peligro | --color-peligro |
shadow-suave | No | --shadows-* no existe | --shadow-suave |
w-tarjeta | Sí, pero no funciona | width: 22 no es CSS válido y el navegador lo ignora | --spacing-tarjeta: 22rem |
Este es el procedimiento, en orden:
- ¿El nombre empieza por un espacio válido? Compáralo con la tabla de la sección 3 o con
theme.css. - ¿El fichero se escanea? Guarda
app.csspara forzarlo. Si la clase está en una ruta excluida, repasa@source. - ¿La regla está en el CSS? Abre DevTools, inspecciona el elemento y busca la clase en el panel de estilos. Si no está, el problema es de compilación. Si está pero tachada, otra regla gana.
- ¿La variable tiene valor? En el panel Computed mira la variable, o escribe en la consola
getComputedStyle(document.documentElement).getPropertyValue('--color-x'). Una cadena vacía significa que no llegó al CSS. - ¿Depende de otra variable? Si el token contiene
var(), piensa en la sección 9.
@theme tampoco admite propiedades ni selectores. Aquí sí hay un error, y es de los pocos claros:
@theme {
color: red;
}
`@theme` blocks must only contain custom properties or `@keyframes`.
La extensión de VS Code del módulo 01 ayuda mucho: si tu token es válido, la clase aparece en el autocompletado. Si no aparece ahí, no existe.
15. Compartir un tema entre proyectos 🔄 v4.0
🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) los temas se comparten con
presets, un objeto JavaScript. Si usas la LTS: publica el objeto y añadepresets: [require('@empresa/tema')]a cadatailwind.config.js; elthemedel proyecto tiene prioridad sobre el del preset.
Una empresa rara vez tiene un solo proyecto. La tienda, el panel interno y la web de empleo deben compartir marca, y copiar el tema a mano garantiza que acaben diferentes. Como en v4 el tema es CSS, se comparte como CSS.
Figura 19. El tema se publica una vez y cada proyecto lo importa.
Un fichero propio
El primer paso, incluso con un solo proyecto, es sacar el tema de app.css:
/* tema.css */
@theme {
--color-marca-500: #0f766e;
--radius-tarjeta: 1rem;
}
/* app.css */
@import "tailwindcss";
@import "./tema.css";
En un repositorio con varios proyectos, la ruta relativa basta: @import "../../paquetes/tema/tema.css";.
Un paquete npm
Para proyectos separados, el fichero se publica como paquete. Basta con que package.json indique dónde está el CSS, con el campo style y la condición style de exports:
{
"name": "@empresa/tema",
"version": "1.0.0",
"style": "./tema.css",
"exports": {
".": { "style": "./tema.css" }
}
}
/* tema.css del paquete */
@theme default {
--color-marca-500: #e8590c;
--radius-tarjeta: 1rem;
}
Fíjate en el default: es la decisión de diseño importante. El proyecto puede ajustar lo que necesite, sin depender del orden de las líneas:
@import "tailwindcss";
@import "@empresa/tema";
@theme {
--radius-tarjeta: 2rem;
}
Con un paquete así en node_modules y vite build, el CSS final contiene --color-marca-500: #e8590c, que viene del paquete, y --radius-tarjeta: 2rem, que es el ajuste del proyecto.
Si el paquete trae también componentes con clases de Tailwind, recuerda que node_modules no se escanea: el proyecto debe añadir @source, como se explicó en el módulo 01.
Evitar choques con un prefijo
Si tu tema convive con otra hoja de estilos que usa los mismos nombres de clase, puedes añadir un prefijo:
@import "tailwindcss" prefix(tw);
@theme {
--color-marca: #0f766e;
}
<div class="tw:bg-marca tw:p-4 tw:hover:flex"></div>
Las clases llevan delante tw:, y las variables también cambian: el CSS usa var(--tw-color-marca). Tú sigues declarando --color-marca en @theme, pero en tu propio CSS debes escribir var(--tw-color-marca), porque un var(--color-marca) escrito a mano no se reescribe.
Compartir en Tailwind 3.4
En 3.4, el preset de la sección 13 se reutiliza así:
// tailwind.config.js del proyecto
module.exports = {
presets: [require('./preset-empresa.cjs')],
content: ['./*.html'],
theme: {
extend: {
colors: { marca: '#123456' }, // gana al preset
},
},
};
Con marca en el preset y en el proyecto, bg-marca usa el del proyecto. rounded-tarjeta, que solo está en el preset, sigue disponible, y el tema por defecto se conserva (bg-red-500). Al declarar presets con contenido, el tema por defecto sigue activo; solo presets: [] lo elimina.
⚠️ Importante: Un preset no se puede usar en v4 con
@import, y untema.csscon@themeno se puede usar en 3.4. Si tienes proyectos en las dos versiones, mantén un fichero de tokens neutro (por ejemplo, un JSON) y genera ambos formatos con un pequeño script, o acepta dos ficheros durante la migración.
16. Ejercicios
Ejercicio 1: Una paleta nueva
Añade al tema dos tonos de un color oliva, #6b8e23 y #4b6319. Crea un botón que use el primero, cambie al segundo con hover y tenga además una etiqueta con el fondo del primero al 30 %.
Ver solución
@import "tailwindcss";
@theme {
--color-oliva-500: #6b8e23;
--color-oliva-700: #4b6319;
}
<button class="rounded-md bg-oliva-500 px-4 py-2 text-white hover:bg-oliva-700">
Añadir
</button>
<span class="rounded-full bg-oliva-500/30 px-2 py-1 text-xs text-oliva-700">Nuevo</span>
No hay que declarar nada más: con el espacio --color-* ya existen bg-, text-, border-, el modificador /30 y la variante hover:.
En 3.4:
theme: { extend: { colors: { oliva: { 500: '#6b8e23', 700: '#4b6319' } } } }
Ejercicio 2: Un tamaño con sus compañeros
Crea un token etiqueta de 0.6875rem, con altura de línea de 1rem, interletraje de 0.06em y peso 600. Comprueba que tracking-normal lo anula en un elemento concreto.
Ver solución
@theme {
--text-etiqueta: 0.6875rem;
--text-etiqueta--line-height: 1rem;
--text-etiqueta--letter-spacing: 0.06em;
--text-etiqueta--font-weight: 600;
}
<span class="text-etiqueta uppercase">Oferta</span>
<span class="text-etiqueta tracking-normal uppercase">Sin interletraje</span>
La clase genera letter-spacing: var(--tw-tracking, var(--text-etiqueta--letter-spacing)). tracking-normal define --tw-tracking, y por eso gana sin importar el orden de las reglas: var() solo recurre al segundo argumento si el primero está vacío.
En 3.4:
fontSize: { etiqueta: ['0.6875rem', { lineHeight: '1rem', letterSpacing: '0.06em', fontWeight: '600' }] }
Ejercicio 3: Ajustar los puntos de ruptura
Añade un punto xs de 30rem y uno 3xl de 120rem, y quita 2xl. Después repite la prueba escribiendo xs como 480px y explica qué orden sale con sm y md.
Ver solución
@theme {
--breakpoint-xs: 30rem;
--breakpoint-3xl: 120rem;
--breakpoint-2xl: initial;
}
Con xs:flex sm:flex 2xl:flex 3xl:flex, el CSS contiene:
@media (width >= 30rem)
@media (width >= 40rem)
@media (width >= 120rem)
2xl:flex ya no genera nada.
Con --breakpoint-xs: 480px y xs:flex sm:block md:grid:
@media (width >= 480px)
@media (width >= 40rem)
@media (width >= 48rem)
En este caso el orden es el correcto, pero por casualidad: 480px son 30 rem y Tailwind coloca los valores en px antes que los rem, sea cual sea su medida. Un punto de 900px también saldría antes que 40rem y rompería el diseño. Por eso conviene usar siempre rem.
Ejercicio 4: Detective de tokens
Este tema no funciona como se esperaba. Encuentra los cinco errores sin compilar y corrígelos.
@theme {
--colors-exito: #16a34a;
--font-size-grande: 3rem;
--color-Peligro: #dc2626;
--spacing-tarjeta: 22;
--shadows-suave: 0 1px red;
}
<div class="bg-exito text-grande bg-peligro w-tarjeta shadow-suave"></div>
Ver solución
@theme {
--color-exito: #16a34a; /* singular */
--text-grande: 3rem; /* el espacio de los tamaños es --text-* */
--color-peligro: #dc2626; /* sin mayúscula, igual que la clase */
--spacing-tarjeta: 22rem; /* con unidad */
--shadow-suave: 0 1px red; /* singular */
}
Con esta versión se generan las cinco clases. El caso más traicionero es --spacing-tarjeta: 22: la clase sí aparece en el CSS, pero width: 22 no es válido y el navegador la ignora. Por eso comprobar que la clase existe no siempre basta.
Ejercicio 5: Sistema cerrado
Crea un tema en el que solo existan marca, gris-100, gris-700 y el blanco. Indica cuáles de estas clases se generan: bg-white, bg-black, bg-red-500, border, bg-transparent, p-4, text-lg.
Ver solución
@theme {
--color-*: initial;
--color-white: #fff;
--color-marca: #0f766e;
--color-gris-100: #f1f5f9;
--color-gris-700: #334155;
}
| Clase | ¿Se genera? | Motivo |
|---|---|---|
bg-white | Sí | La hemos vuelto a declarar |
bg-black | No | Era parte de la paleta borrada |
bg-red-500 | No | Igual |
border | Sí | No depende de un color del tema |
bg-transparent | Sí | En v4 no es un token |
p-4 | Sí | Solo hemos borrado --color-* |
text-lg | Sí | Igual |
En 3.4 habría que escribir theme.colors sin extend, añadiendo transparent y current a mano.
Ejercicio 6: Aviso por zona
Tienes --aviso: #b45309 en :root. Dentro de .panel-critico debe valer #b91c1c. Crea un token para usar bg-aviso, bg-aviso/20 y text-aviso que respete el cambio de zona.
Ver solución
@theme inline {
--color-aviso: var(--aviso);
}
:root {
--aviso: #b45309;
}
.panel-critico {
--aviso: #b91c1c;
}
Genera background-color: var(--aviso), que se evalúa en cada elemento. Con @theme normal, --color-aviso se resolvería en :root y todos los avisos quedarían ámbar, también los del panel.
En 3.4, usa aviso: 'rgb(var(--aviso) / <alpha-value>)' con --aviso: 180 83 9, o bg-aviso/20 no se generará.
Ejercicio 7: Colores para un gráfico
Un script recorre --color-grafico-1, -2 y -3 construyendo el nombre con un índice. Ninguna clase los usa. Escribe el tema y el código de lectura para que los tres valores lleguen.
Ver solución
@theme static {
--color-grafico-1: #0f766e;
--color-grafico-2: #b45309;
--color-grafico-3: #6d28d9;
}
const estilos = getComputedStyle(document.documentElement);
const colores = [1, 2, 3].map((i) => estilos.getPropertyValue(`--color-grafico-${i}`));
console.log(colores);
[ '#0f766e', '#b45309', '#6d28d9' ]
Sin static obtendrías tres cadenas vacías, porque el escáner nunca ve los nombres completos.
Ejercicio 8: Traducir una configuración
Traduce este fichero de Tailwind 3.4 a @theme. Decide tú qué líneas ya no hacen falta.
export default {
theme: {
screens: { sm: '40rem', lg: '64rem' },
extend: {
colors: { marca: { DEFAULT: '#0f766e', oscuro: '#134e4a' } },
fontFamily: { titulares: ['Georgia', 'serif'] },
fontSize: { etiqueta: ['0.75rem', { lineHeight: '1rem', fontWeight: '600' }] },
spacing: { 18: '4.5rem', ficha: '22rem' },
borderRadius: { tarjeta: '1rem' },
boxShadow: { suave: '0 1px 3px rgb(0 0 0 / 0.1)' },
zIndex: { modal: '50' },
aspectRatio: { producto: '4 / 3' },
keyframes: { latir: { '50%': { opacity: '0.5' } } },
animation: { latir: 'latir 1s ease-in-out infinite' },
},
},
};
Ver solución
@import "tailwindcss";
@theme {
/* screens sin extend: reinicia y declara */
--breakpoint-*: initial;
--breakpoint-sm: 40rem;
--breakpoint-lg: 64rem;
--color-marca: #0f766e;
--color-marca-oscuro: #134e4a;
--font-titulares: Georgia, serif;
--text-etiqueta: 0.75rem;
--text-etiqueta--line-height: 1rem;
--text-etiqueta--font-weight: 600;
--spacing-ficha: 22rem;
--radius-tarjeta: 1rem;
--shadow-suave: 0 1px 3px rgb(0 0 0 / 0.1);
--z-index-modal: 50;
--aspect-producto: 4 / 3;
--animate-latir: latir 1s ease-in-out infinite;
@keyframes latir {
50% {
opacity: 0.5;
}
}
}
Las dos versiones se comportan igual con estas clases: bg-marca, bg-marca-oscuro, font-titulares, text-etiqueta, p-18, w-ficha, rounded-tarjeta, shadow-suave, z-modal, aspect-producto, animate-latir, sm:flex y lg:flex se generan en ambas, y md:flex en ninguna.
La línea que sobra es spacing: { 18: '4.5rem' }. En v4 la escala es dinámica, así que p-18 existe ya y vale calc(var(--spacing) * 18), que son los mismos 4.5rem.
Dos detalles que se olvidan con facilidad: zIndex pasa a --z-index-*, y DEFAULT desaparece porque --color-marca ya significa “sin sufijo”.
Ejercicio 9: Tema con valores de partida
Publica un tema compartido con --radius-tarjeta: 1rem y haz que un proyecto lo cambie a 2rem, de manera que el resultado no dependa de si escribe su @theme antes o después del @import.
Ver solución
/* tema.css del paquete */
@theme default {
--radius-tarjeta: 1rem;
}
/* app.css del proyecto */
@import "tailwindcss";
@import "@empresa/tema";
@theme {
--radius-tarjeta: 2rem;
}
default hace que el valor ceda ante cualquier @theme normal, en cualquier posición. Si el paquete usara @theme sin más, ganaría el último bloque, y un @import puesto después pisaría el ajuste del proyecto.
Ejercicio 10 (propuesto): Una tercera marca
Añade a la tienda de la sección 13 una marca data-marca="premium" con otro color primario. Debes declarar un tercer tono, calcular su contraste con --sobre-primario y no tocar ninguna clase del HTML. Comprueba que data-tema="oscuro" y data-marca="premium" funcionan a la vez en el mismo <html>. Piensa qué regla debería ganar si las dos redefinen --primario.
Ejercicio 11 (propuesto): Modo compacto
Añade al panel de pedidos un conmutador que ponga data-densidad="compacta" en <main>. Esa regla debe cambiar --spacing a 0.2rem en esa zona. Haz después el mismo ejercicio en Tailwind 3.4 y explica por qué no se puede resolver igual.
Resumen
| Concepto | API | Idea clave |
|---|---|---|
| Declarar el tema | @theme { --color-marca: ...; } | Genera una variable CSS y sus clases |
| Equivalente en 3.4 | theme.extend en tailwind.config.js | Sin extend, reemplazas la escala entera |
| Espacios de nombres | --color-*, --font-*, --text-*, --radius-*… | El prefijo decide qué utilidades lo usan; un prefijo desconocido falla en silencio |
| Fuentes | --font-*, @font-face fuera de @theme | El token no descarga la fuente |
| Subvalores | --text-x--line-height | Valores por defecto que leading-* y compañía sobrescriben |
| Densidad | --spacing | Toda la escala es calc(var(--spacing) * n); admite cambios por zona |
| Puntos de ruptura | --breakpoint-* | Crea x:, max-x: y min-x:; usa rem |
| Por defecto | --default-font-family, --default-ring-width | Valores de utilidades sin sufijo |
| Uso en CSS | var(), --theme(), --spacing(), --alpha() | --theme() falla al compilar si el nombre no existe |
| Uso en JavaScript | getComputedStyle(...).getPropertyValue() | Un nombre calculado necesita static |
| Variables usadas | @theme | Solo salen las usadas en clases, en tu CSS o escritas enteras en un fuente |
| Todas las variables | @theme static, theme(static) | Salen siempre, a costa de bytes |
| Tokens encadenados | @theme inline | Copia el valor en la clase para evitar que se resuelva en :root |
| Sin emitir | @theme reference | Otra hoja ya declara la variable |
| Tema compartido | @theme default | Cede ante cualquier @theme normal, en cualquier orden |
Dentro de @media | @theme | No se aplica la condición, y no hay aviso |
| Reiniciar | --color-*: initial, --*: initial | bg-transparent sobrevive en v4 y no en v3 |
| Marca | Primitivos, semánticos y clases | El HTML nombra papeles; el tema decide los colores |
| Canales en 3.4 | rgb(var(--x) / <alpha-value>) | Sin esto, bg-x/50 no se genera |
| Compartir | @import "@empresa/tema", presets en 3.4 | En v4 el tema es CSS y se distribuye como CSS |
| Prefijo | @import "tailwindcss" prefix(tw) | Clases tw: y variables --tw-* |