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 @theme y traducirlo a tailwind.config.js cuando 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-* toca background-color; rounded-*, border-radius.
  • El tema guarda los valores permitidos: qué es indigo-600, cuánto mide lg, a qué anchura empieza md.

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.css sin 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 en theme.extend dentro de tailwind.config.js; ese fichero es obligatorio en 3.4 y tailwindcss init lo 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.

Del bloque @theme salen una variable en :root y las clases bg, text, border, ring y demás

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-500 no generaría nada. @theme le 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-marca se convierte en theme.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-suave genera text-texto-suave. Si la repetición te molesta, no es un error de Tailwind: es que el prefijo text- y el token dicen lo mismo. Muchos equipos prefieren --color-suave para escribir text-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-marca o --font-size-grande se 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.colors y los valores oklch() 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 objeto marca.

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>

Escala de once tonos verde azulado del 50 al 950 y cinco botones que usan esos colores como fondo, borde, opacidad y anillo

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 fontFamily como lista y los tamaños en fontSize como par [tamaño, opciones]. Si usas la LTS: escribe fontSize: { 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>

A la izquierda un título con text-4xl normal; a la derecha uno con font-titulares y text-titulo, más grueso y compacto. Debajo, un precio en negrita y su variante ligera

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 en theme.spacing con 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>

Tres tarjetas idénticas con --spacing de 0.2rem, 0.25rem y 0.32rem: la primera compacta y la tercera holgada

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-tarjeta usa 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>

Tres tarjetas: una con los valores por defecto y dos con el radio y la sombra de la marca, más redondeadas y con sombra más profunda

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>

A 700 píxeles de ancho las tres tarjetas se apilan en una columna

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

A 860 píxeles de ancho las tres tarjetas forman una fila

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 en px antes que todos los puntos en rem, 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 usa theme(colors.marca.500 / 50%) y theme(spacing.4), y en JavaScript resolveConfig de tailwindcss/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 --spacing en 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);
});

Gráfico de barras en un canvas con los colores de la marca y debajo el valor leído de --color-marca-500

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:

  1. La usa una clase presente en tus ficheros, como bg-x.
  2. Aparece en tu propio CSS, como color: var(--color-y).
  3. 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 static por 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, usa rgb(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.

Tres zonas: con @theme normal la tarjeta sigue blanca; con @theme inline y con el token sobrescrito directamente la tarjeta es oscura

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.

Dos cadenas de elementos: en la izquierda --color-caja se calcula en root y llega blanca a la tarjeta; en la derecha la tarjeta lee --superficie directamente

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: @theme no se puede meter en un @media para 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 #000 se aplica siempre, también en modo claro. Lo mismo pasa si lo metes dentro de un selector. Declara @theme siempre 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 default es un preset, porque el theme del proyecto ya tiene prioridad sobre él. reference no 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:

Tabla con los modos normal, static, inline y reference: qué variable sale en :root, qué regla genera la clase y cuándo usarlo

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:

  1. Dos bloques normales: gana el último.
  2. Un bloque default: cede siempre ante uno normal.
  3. @theme antes de @import "tailwindcss": tu valor sigue ganando, porque el tema de Tailwind es default. 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 en theme sin extend, y todo el tema con presets: []. Si usas la LTS: para quitar los colores usa theme: { 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>

Cinco botones sobre fondo papel: bg-marca y bg-marca/15 tienen color, bg-red-500 y bg-slate-200 aparecen sin fondo, y bg-transparent con anillo funciona

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-*: initial es razonable; --*: initial casi 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>

Tienda con tres tarjetas de producto, fuente de sistema y botones índigo: el aspecto genérico de Tailwind

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:

Tres columnas: primitivos con los valores crudos, semánticos que cambian según el tema, y las clases que usa el HTML

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>

La misma tienda con titulares en Georgia, tarjetas muy redondeadas y botones en verde azulado oscuro

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

La tienda en modo oscuro con fondo verde muy oscuro, tarjetas algo más claras y botones claros

Figura 16. data-tema="oscuro". No hay ni un dark: en el HTML.

<html data-marca="outlet">

La tienda con la marca outlet: botones naranjas con texto oscuro y etiquetas en verde azulado

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) y dark: 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;
  }
}

La tienda compilada con Tailwind 3.4.19, con el mismo aspecto que la versión 4.3

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:

  1. ¿El nombre empieza por un espacio válido? Compáralo con la tabla de la sección 3 o con theme.css.
  2. ¿El fichero se escanea? Guarda app.css para forzarlo. Si la clase está en una ruta excluida, repasa @source.
  3. ¿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.
  4. ¿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.
  5. ¿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ñade presets: [require('@empresa/tema')] a cada tailwind.config.js; el theme del 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.

Un paquete tema.css con @theme default del que parten dos proyectos: la tienda lo usa tal cual y el panel interno lo ajusta

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 un tema.css con @theme no 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-*