Módulo 06: Bordes, sombras y efectos

Objetivos del módulo

  • Dibujar bordes con border-* y entender por qué basta una clase gracias a Preflight
  • Redondear esquinas con rounded-* y recortar el contenido sin sorpresas
  • Distinguir border, ring, outline y shadow, y elegir el adecuado en cada caso
  • Crear indicadores de foco que funcionen con teclado y en colores forzados
  • Separar elementos con divide-* y conocer sus trampas en v4
  • Construir sombras por capas con shadow-*, inset-shadow-* e inset-ring-*
  • Controlar la transparencia con opacity-* y mezclar capas con mix-blend-*
  • Aplicar filtros con blur-*, grayscale, drop-shadow-* y sus versiones backdrop-*
  • Prever los efectos secundarios de estas clases: contextos de apilamiento y position: fixed
  • Recortar elementos con máscaras mask-* y escalarlos con zoom-*
  • Conocer las escalas renombradas de v3 a v4, incluidos los falsos amigos que no dan error

1. Preparación del laboratorio

Todo el módulo se apoya en el proyecto Vite del módulo 01. Los ejemplos usan cuatro imágenes propias. Descárgalas y guárdalas en public/img/ con estos nombres, para que Vite las sirva en /img/...:

Descarga Guárdala como Para qué sirve
06-img-escena.svg public/img/escena.svg Imagen de fondo con muchos colores, ideal para filtros
06-img-portatil.svg public/img/portatil.svg Imagen con transparencia y forma irregular
06-img-torre.svg public/img/torre.svg Producto de la tienda
06-img-teclado.svg public/img/teclado.svg Producto de la tienda

Para que cada figura sea legible, los ejemplos usan algunas clases que aún no has estudiado. Con esta tabla te basta por ahora:

Clases Qué hacen Dónde se estudian
flex, grid, grid-cols-3, items-center, justify-between, gap-4 Colocan los elementos Flexbox y Grid
relative, absolute, inset-0, z-10, overflow-hidden Posicionan y recortan Layout y posicionamiento
hover:, focus-visible:, disabled: Aplican una clase solo en ese estado Variantes y estados
dark: Aplica una clase en modo oscuro Modo oscuro
transition-shadow, duration-300 Animan el cambio Transiciones y animaciones

⚠️ Importante: Este es el módulo con más falsos amigos de todo el curso: clases que existen en v3 y en v4 con distinto resultado y que no dan ningún error. Cada sección los señala con su marca, y la sección 19 los reúne en una tabla.

Si quieres repasar antes las propiedades CSS de base, consulta el modelo de cajas del Curso de HTML y CSS.


2. Bordes: border-*

Por qué basta con una clase

En CSS puro, border-width: 1px no dibuja nada, porque el navegador parte de border-style: none. Tailwind lo resuelve desde Preflight, la hoja de reinicio que viste en el módulo 02. Esta regla se aplica a todos los elementos:

/* Preflight de Tailwind 4.3.3 */
*,
::after,
::before,
::backdrop,
::file-selector-button {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  border: 0 solid;
}

Todo elemento nace con borde de ancho 0 y estilo solid. La clase border solo necesita dar el ancho, y el borde aparece:

/* Lo que genera la clase border en 4.3.3 */
.border {
  border-style: var(--tw-border-style);
  border-width: 1px;
}

La variable --tw-border-style vale solid por defecto. Existe para que border-dashed siga funcionando aunque otra clase vuelva a escribir border-style.

Muestrario de anchos, lados, estilos y colores de borde

Figura 1. Anchos, lados, estilos y colores. border-3 es un valor dinámico de v4 (ver la sección 20).

Ancho y lados

Clase CSS Efecto
border border-width: 1px Los cuatro lados
border-0, border-2, border-4, border-8 border-width: 0, 2px, 4px, 8px Otros anchos
border-t, border-r, border-b, border-l border-top-width: 1px… Un lado físico
border-x, border-y Los dos laterales, o arriba y abajo Dos lados opuestos
border-s, border-e border-inline-start-width, border-inline-end-width Inicio y fin de línea
border-t-4, border-s-2… El lado con ese ancho Todas las combinaciones

Los lados s (start) y e (end) son lógicos: dependen de la dirección del texto. En español, border-s es el lado izquierdo; en árabe o hebreo (dir="rtl"), el derecho. Si tu aplicación puede traducirse a esos idiomas, usa s y e en lugar de l y r. Las propiedades lógicas se presentaron en el módulo 03.

Estilo

Clase CSS Observación
border-solid border-style: solid Por defecto, gracias a Preflight
border-dashed border-style: dashed Discontinuo: zonas para soltar ficheros, estados vacíos
border-dotted border-style: dotted Punteado
border-double border-style: double Doble línea: necesita al menos 3 px para distinguirse
border-none border-style: none Sin borde, aunque tenga ancho
border-hidden border-style: hidden Igual que none, salvo en tablas colapsadas, donde gana a los bordes vecinos

border-0 y border-none parecen iguales, pero actúan sobre propiedades distintas. Usa border-0 para quitar un borde que da otra clase, y border-none solo cuando necesites cambiar el estilo.

Color

Los colores usan la paleta del módulo 05 y admiten modificador de opacidad:

<div class="border-2 border-rose-500/30">Con el 30 % de opacidad</div>
<div class="border-4 border-t-rose-500 border-r-amber-400 border-b-teal-500 border-l-indigo-500">Un color por lado</div>
<div class="border-2 border-current">El color del texto</div>
<div class="border-2 border-transparent">Invisible, pero ocupa su sitio</div>

Color por defecto 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), un borde sin color es gris (gray-200); en v4 usa currentColor, el color del texto. Si usas la LTS: no tienes que hacer nada si quieres ese gris, y si quieres el color del texto añade border-current.

Con la misma clase border y text-indigo-700, esto es lo que devuelve el navegador:

Tailwind 3.4.19   border-top-color: rgb(229, 231, 235)        (gris 200)
Tailwind 4.3.3    border-top-color: oklch(0.457 0.24 277.023) (el del texto, indigo-700)

El motivo está en Preflight: el de v3 fija border-color con el gris del tema, y el de v4 escribe solo border: 0 solid, que deja el color por defecto del navegador. El cambio afecta también a divide-*.

Si migras un proyecto grande y no quieres revisar cada borde, esta regla recupera el gris de v3. La hemos probado: los bordes sin color vuelven a gray-200.

@layer base {
  *,
  ::after,
  ::before,
  ::backdrop,
  ::file-selector-button {
    border-color: var(--color-gray-200, currentcolor);
  }
}

💡 Consejo: En código nuevo para v4, escribe siempre el color: border border-slate-200. Queda explícito, y el mismo HTML se ve igual en las dos versiones.

Los lados de bloque: border-bs-* y border-be-* 🆕 v4.2

🆕 Desde Tailwind 4.2. No existe en la LTS (Tailwind 3.4). Si usas la LTS: usa border-t-* y border-b-*, que coinciden salvo en escritura vertical, o escribe la propiedad con [border-block-start-width:2px].

border-s y border-e son lógicos en el eje de la línea. border-bs y border-be lo son en el eje de bloque: en español, arriba y abajo. Escriben border-block-start-width y border-block-end-width, y aceptan color (border-bs-red-500). Solo notarás la diferencia con border-t en textos verticales.

El borde ocupa sitio

Preflight pone box-sizing: border-box, de modo que el ancho que das a una caja incluye su borde. El borde no agranda la caja, le quita espacio al contenido:

<div class="w-40 border-8 border-slate-300 p-2">caja</div>
Medido en Chromium:
ancho de la caja: 160 px   (w-40)
ancho interior:   144 px   (160 - 8 - 8; incluye el padding)

Pero la altura sí crece si no la fijas, y esto causa un defecto habitual: al añadir un borde en un estado concreto, todo se mueve. La solución es reservar el sitio con un borde transparente:

<!-- Mal: al pasar el ratón, el borde aparece y la tarjeta crece 2 px por cada lado -->
<a class="rounded-lg p-4 hover:border-2 hover:border-indigo-500">Portátiles</a>

<!-- Bien: el borde existe siempre; solo cambia de color -->
<a class="rounded-lg border-2 border-transparent p-4 hover:border-indigo-500">Portátiles</a>

Este borde transparente tiene una ventaja extra: con colores forzados (Windows en alto contraste) el navegador sustituye transparent por un color visible. Lo medimos: border-transparent pasa de rgba(0, 0, 0, 0) a rgb(0, 0, 0). Tu tarjeta conserva así un contorno aunque el modo elimine los fondos.

Borde semitransparente sobre fondo de color

El fondo de una caja se pinta por defecto también debajo del borde, y un borde semitransparente lo deja ver. Con bg-clip-padding, el fondo acaba donde empieza el borde (background-clip pasa de border-box a padding-box), y el borde se mezcla con lo que hay detrás de la caja:

<div class="w-40 border-4 border-black/50 bg-amber-300 bg-clip-padding p-2">Borde sobre lo que hay detrás</div>

Opacidad de borde: border-opacity-* eliminada 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) existen border-opacity-*, divide-opacity-* y ring-opacity-*; en v4 están eliminadas. Si usas la LTS: siguen funcionando, pero escribe ya border-black/50, divide-black/50 y ring-black/50, que van en las dos versiones.


3. Esquinas: rounded-*

rounded-* escribe la propiedad border-radius. Admite la caja entera, un lado o una esquina, y funciona aunque el elemento no tenga borde.

Muestrario de radios, lados, propiedades lógicas y recorte con overflow-hidden

Figura 2. Escala de radios de v4, lados, esquinas, propiedades lógicas y el efecto de overflow-hidden.

La escala de radios 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), rounded-sm vale 2 px, y en v4 vale 4 px: el mismo nombre da otro resultado. Si usas la LTS: usa rounded-sm para 2 px y rounded para 4 px. No tienes rounded-xs ni rounded-4xl; este último se escribe rounded-[2rem].

Clase en v4 Variable del tema Valor Clase en v3
rounded-none   0 rounded-none
rounded-xs --radius-xs 0.125rem (2 px) rounded-sm
rounded-sm --radius-sm 0.25rem (4 px) rounded
rounded-md --radius-md 0.375rem (6 px) rounded-md
rounded-lg --radius-lg 0.5rem (8 px) rounded-lg
rounded-xl --radius-xl 0.75rem (12 px) rounded-xl
rounded-2xl --radius-2xl 1rem (16 px) rounded-2xl
rounded-3xl --radius-3xl 1.5rem (24 px) rounded-3xl
rounded-4xl --radius-4xl 2rem (32 px) rounded-[2rem]
rounded-full   calc(infinity * 1px) rounded-full (9999px)

La clase rounded sin sufijo sigue funcionando en v4 y vale lo mismo que en v3, 4 px. Pero en el tema de v4 figura en un bloque marcado como Deprecated (obsoleto), así que usa rounded-sm.

rounded-full da una píldora si la caja es alargada y un círculo si es cuadrada: size-20 rounded-full.

Lados y esquinas

Clase Qué redondea
rounded-t-lg, rounded-r-lg, rounded-b-lg, rounded-l-lg Las dos esquinas de ese lado
rounded-tl-lg, rounded-tr-lg, rounded-br-lg, rounded-bl-lg Una esquina física
rounded-s-lg, rounded-e-lg Lado de inicio o de fin de línea (lógico)
rounded-ss-lg, rounded-se-lg, rounded-ee-lg, rounded-es-lg Una esquina lógica

Fíjate en la tercera fila de la Figura 2: con dir="rtl", el mismo rounded-s-2xl redondea el otro lado. Las esquinas lógicas se leen como inicio de bloque + inicio de línea: ss es arriba a la izquierda en español, y ee, abajo a la derecha.

Radios y contenido: overflow-hidden

border-radius redondea la caja, pero no recorta a sus hijos. Una imagen dentro de una tarjeta redondeada sigue siendo rectangular y asoma por las esquinas. Hay dos soluciones:

<!-- 1. El padre recorta a todos sus hijos: suele ser lo mejor -->
<article class="overflow-hidden rounded-2xl border border-slate-300">
  <img src="/img/escena.svg" alt="" class="h-24 w-full object-cover">
  <p class="p-2">El padre recorta a sus hijos</p>
</article>

<!-- 2. Redondear al hijo con el mismo radio: útil si no puedes usar overflow-hidden -->
<article class="rounded-2xl border border-slate-300">
  <img src="/img/escena.svg" alt="" class="h-24 w-full rounded-t-2xl object-cover">
  <p class="p-2">El hijo se redondea por su cuenta</p>
</article>

⚠️ Importante: overflow-hidden también recorta todo lo que se dibuje fuera del padre: menús desplegables, tooltips, y las sombras y anillos de los hijos. Lo veremos en la sección 6. Si tu tarjeta contiene un menú desplegable, usa la opción 2.

Tablas: rounded-* no funciona con border-collapse

Preflight pone border-collapse: collapse en las tablas, y con bordes colapsados el navegador ignora border-radius. Es lo que muestra la columna derecha de la Figura 2. La solución es meter la tabla en un contenedor con overflow-hidden rounded-2xl border. Las tablas se tratan a fondo en el módulo 17.


4. Anillos: ring-*

Un anillo es una línea alrededor de la caja que no ocupa sitio. No es un borde de verdad: Tailwind lo dibuja con una sombra sólida, sin difuminado.

📘 Concepto: box-shadow: 0 0 0 3px azul es una sombra sin desplazamiento (0 0), sin difuminado (el tercer 0) y con 3 px de extensión. La extensión agranda la sombra por igual en todas direcciones, y el resultado parece un borde exterior.

Comparación de una marca hecha con border, ring y outline, muestrario de anillos y contornos, y separación sobre fondo oscuro

Figura 3. Arriba, la misma marca de 4 px hecha de tres formas: solo el borde mueve a sus vecinos. Abajo, sobre fondo oscuro, el hueco de ring-offset es en realidad un color.

Los anillos se adaptan al border-radius y se pueden combinar con una sombra normal gracias a las capas que verás en la sección 10.

Ancho y color por defecto 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), ring mide 3 px y es azul (blue-500 al 50 %); en v4 mide 1 px y usa currentColor, igual que border. Si usas la LTS: escribe siempre ring-2 o ring-4 y un color explícito (ring-indigo-500), que da el mismo resultado en las dos versiones. La clase ring-3 no existe; usa ring-[3px].

Es uno de los falsos amigos peligrosos, porque ring existe en las dos versiones:

ring en 3.4.19: rgba(59, 130, 246, 0.5) 0px 0px 0px 3px
ring en 4.3.3:  oklch(0.457 0.24 277.023) 0px 0px 0px 1px   (con text-indigo-700)

El resto de la escala coincide en las dos versiones: ring-0, ring-1, ring-2, ring-4 y ring-8. ring-3 solo existe en v4.

Si migras y no quieres tocar el código, estas variables de tema recuperan el comportamiento de v3. Las hemos probado y funcionan, aunque la guía oficial avisa de que existen solo por compatibilidad:

@theme {
  --default-ring-width: 3px;
  --default-ring-color: var(--color-blue-500);
}

Color y opacidad

Funcionan como en los bordes: ring-indigo-500, ring-indigo-500/40 y ring-current.

Anillo interior: ring-inset

ring-inset dibuja el anillo hacia dentro de la caja, sobre su fondo. Sirve cuando el elemento está pegado a un borde y un anillo exterior quedaría cortado, por ejemplo en una celda de tabla enfocada.

<div class="rounded-lg bg-slate-100 p-3 ring-4 ring-inset ring-indigo-500">Anillo interior</div>

Separación: ring-offset-*

ring-offset-2 separa el anillo de la caja, pero no crea un hueco real: pinta otra sombra sólida, blanca por defecto, entre la caja y el anillo. Sobre un fondo blanco no se nota. Sobre uno oscuro aparece una franja blanca, como en la Figura 3.

<!-- Mal sobre fondo oscuro: el hueco se ve blanco -->
<button class="ring-2 ring-indigo-300 ring-offset-2">Comprar</button>

<!-- Bien, pero hay que repetir el color del fondo -->
<button class="ring-2 ring-indigo-300 ring-offset-2 ring-offset-slate-900">Comprar</button>

💡 Consejo: Si quieres separación, usa outline-offset-*, que deja un hueco de verdad: se ve lo que haya detrás, sea del color que sea. Es la tercera tarjeta oscura de la Figura 3, y no te obliga a repetir el color del fondo.


5. Contornos: outline-*

outline es la propiedad nativa de CSS para contornos. Como el anillo, no ocupa sitio. Se distingue de él en dos cosas: admite un hueco real (outline-offset) y sobrevive a los colores forzados, como veremos en la sección 7.

outline y outline-2 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), outline-2 solo da el ancho y no se ve sin la clase outline, que además mide 3 px por sí sola; en v4, outline mide 1 px y outline-2 funciona sola. Si usas la LTS: escribe siempre las dos clases, outline outline-2. La clase outline-solid no existe, porque outline ya es sólido.

Medido con las mismas clases en Chromium:

Clases v3.4.19 v4.3.3
outline solid 3px solid 1px
outline-2 none 2px (invisible) solid 2px
outline outline-2 solid 2px solid 2px

La tabla explica un fallo frecuente al seguir tutoriales: copias focus:outline-2 de un ejemplo de v4 en un proyecto v3 y no aparece nada.

Estilo, color y separación

Clase CSS Observación
outline-dashed, outline-dotted, outline-double outline-style Cambian el estilo
outline-indigo-600, outline-indigo-600/50 outline-color Sin color, usa currentColor
outline-offset-2 outline-offset: 2px Hueco real entre caja y contorno
-outline-offset-2 outline-offset: -2px Contorno hacia dentro de la caja

outline-none y outline-hidden 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), outline-none crea un contorno transparente que reaparece en colores forzados; en v4 esa misma clase elimina el contorno del todo, y lo de antes se llama outline-hidden. Si usas la LTS: sigue usando outline-none; es la opción accesible y outline-hidden no existe.

/* outline-none en 3.4.19 */
.outline-none {
  outline: 2px solid transparent;
  outline-offset: 2px;
}

/* outline-none en 4.3.3: solo outline-style: none, sin contorno de ningún tipo */

/* outline-hidden en 4.3.3: equivale al outline-none de v3 */
.outline-hidden {
  --tw-outline-style: none;
  outline-style: none;
  @media (forced-colors: active) {
    outline: 2px solid transparent;
    outline-offset: 2px;
  }
}

Por tanto, al copiar código de v3 a v4 una clase idéntica pierde silenciosamente un mecanismo de accesibilidad. La regla es sencilla: en v4, outline-hidden para quitar el contorno del navegador. Deja outline-none para casos en los que de verdad no quieras contorno en ningún modo.


6. Borde, anillo, contorno o sombra: cómo elegir

Ya tienes cuatro formas de dibujar una línea alrededor de una caja. Parecen intercambiables, pero no lo son.

Diagrama de una caja con borde dentro, anillo fuera y contorno con hueco, junto a una tabla comparativa

Figura 4. Dónde se pinta cada línea y cómo se comporta. Todas las filas de la tabla se han medido en Chromium.

Hemos medido la Figura 3 con getBoundingClientRect() para confirmar la fila del espacio. El elemento marcado es “Sobremesa”, y su vecino, “Periféricos”:

                 Sobremesa (ancho x alto)   Periféricos (posición x)
sin marca        91,4 x 32                  333,4
border-4         99,4 x 40                  341,4    <- crece 8 px y empuja 8 px
ring-4           91,4 x 32                  333,4
outline-4        91,4 x 32                  333,4
Necesitas… Usa
Una línea que forme parte del diseño y siempre esté ahí border
Solo un lado, o colores distintos por lado border-t, border-s…
Resaltar un elemento sin mover nada ring u outline
Un perfilado sutil, junto con una sombra ring-1 ring-black/5
El indicador de foco outline (ver la sección siguiente)
Sensación de altura shadow

Lo que recorta overflow-hidden

Si un padre tiene overflow-hidden y el hijo llega hasta su borde, todo lo que el hijo pinte fuera de su caja desaparece: anillos, contornos y sombras. El borde se salva porque está dentro de la caja.

<div class="overflow-hidden">
  <div class="ring-4 ring-rose-500">Este anillo no se ve</div>
</div>

<!-- Soluciones: dejar margen en el padre (overflow-hidden p-1), o dibujar hacia dentro -->
<div class="overflow-hidden">
  <div class="ring-4 ring-inset ring-rose-500">Con ring-inset no sale de la caja</div>
</div>

7. Foco visible y accesibilidad 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), outline-2 necesita la clase outline y outline-none ya es accesible. Si usas la LTS: escribe focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 para el foco, y usa outline-none donde esta sección dice outline-hidden.

Quien navega con teclado necesita ver en qué elemento está. Es la función del indicador de foco, y es un requisito de accesibilidad (WCAG). Preflight no lo quita: si no haces nada, el navegador dibuja el suyo (outline: auto, medido en las dos versiones).

Cuando quieras uno propio, hay tres formas habituales. Las hemos probado de verdad: pulsando el tabulador con Playwright, una vez en modo normal y otra emulando colores forzados, el modo que usa Windows en alto contraste. En ese modo el navegador impone su paleta y elimina todas las sombras, y los anillos son sombras.

Tres botones enfocados con el tabulador en modo normal y en colores forzados

Figura 5. En modo normal las tres opciones se ven iguales. Con colores forzados, la primera se queda sin indicador de foco.

Foco medido en colores forzados (Chromium):
outline-none + ring       box-shadow: none   outline: none          <- no se ve el foco
outline-hidden + ring     box-shadow: none   outline: solid 2px     <- se ve
outline-3 en foco         box-shadow: none   outline: solid 3px     <- se ve

La primera fila es un falso amigo: con las mismas clases, en v3 sí quedaba un contorno visible en colores forzados, porque su outline-none no eliminaba nada. Lo medimos también:

outline-none + ring en colores forzados:
v3.4.19   outline: solid 2px   <- se ve
v4.3.3    outline: none        <- no se ve

Este es el código de los tres botones:

<!-- 1. MAL en v4: en colores forzados no queda ningún indicador -->
<button class="outline-none focus-visible:ring-3 focus-visible:ring-indigo-400 focus-visible:ring-offset-2">
  Añadir al carrito
</button>

<!-- 2. Aceptable: el anillo se ve en modo normal y outline-hidden entra en colores forzados -->
<button class="outline-hidden focus-visible:ring-3 focus-visible:ring-indigo-400 focus-visible:ring-offset-2">
  Añadir al carrito
</button>

<!-- 3. Recomendado: un contorno de verdad -->
<button class="focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-indigo-400">
  Añadir al carrito
</button>

La opción 3 es la más sencilla: no hay que quitar nada, no hay que repetir colores de fondo y funciona en cualquier modo.

💡 Consejo: Usa focus-visible: y no focus:. El navegador solo muestra focus-visible cuando el foco tiene sentido visual, como al usar el teclado, y no al hacer clic con el ratón en un botón. Tienes los detalles en el módulo 11.

Contraste del indicador

Comprueba que el contorno se distingue de lo que tiene alrededor. La pauta WCAG 2.2 pide un contraste mínimo de 3:1 para los componentes de la interfaz. Un outline-white en un botón situado sobre fondo blanco no sirve, aunque el código sea correcto. El hueco de outline-offset-2 ayuda mucho, porque separa el contorno del color del propio botón.


8. Separadores: divide-*

Cuando tienes una lista y quieres una línea entre sus elementos, poner border-b a cada uno deja una línea sobrante en el último. divide-y, aplicado al padre, dibuja solo las líneas intermedias.

Lista, fila de datos y tabla con separadores, y el defecto del último elemento oculto

Figura 6. divide-y, divide-x, divide-y en una tabla y, abajo a la derecha, el defecto del último hijo oculto en v4.

<ul class="divide-y divide-slate-200 rounded-xl border border-slate-200">
  <li class="flex justify-between p-3"><span>Portátil UltraBook 14</span><span>899,00 €</span></li>
  <li class="flex justify-between p-3"><span>Ratón inalámbrico</span><span>19,90 €</span></li>
  <li class="flex justify-between p-3"><span>Teclado mecánico</span><span>79,90 €</span></li>
</ul>

En horizontal, con divide-x:

<dl class="flex divide-x divide-slate-200 rounded-xl border border-slate-200 text-center">
  <div class="flex-1 p-3"><dt>Pedidos</dt><dd class="text-2xl font-bold">128</dd></div>
  <div class="flex-1 p-3"><dt>Clientes</dt><dd class="text-2xl font-bold">47</dd></div>
  <div class="flex-1 p-3"><dt>Productos</dt><dd class="text-2xl font-bold">36</dd></div>
</dl>

En una tabla, se aplica a <tbody>, cuyos hijos son las filas: <tbody class="divide-y divide-slate-200">.

Clase Qué hace
divide-y, divide-x Línea de 1 px entre hijos
divide-y-2, divide-x-4… Otros anchos
divide-slate-200, divide-slate-200/50 Color y opacidad
divide-dashed, divide-dotted, divide-double, divide-solid, divide-none Estilo
divide-y-reverse, divide-x-reverse Para hijos en orden inverso

Hijos en orden inverso

Si los hijos se muestran al revés con flex-col-reverse, las líneas caen en el lado equivocado. divide-y-reverse lo corrige:

<ul class="flex flex-col-reverse divide-y divide-y-reverse divide-rose-500">
  <li>Primero en el HTML, último en pantalla</li>
  <li>Segundo</li>
  <li>Tercero</li>
</ul>

Un selector distinto en cada versión 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), el divisor es un borde superior en cada hijo menos el primero, con más prioridad que las clases del hijo; en v4 es un borde inferior en cada hijo menos el último, y las clases del hijo ganan. Si usas la LTS: no te afecta el defecto del último hijo oculto; para anular un divisor en un hijo concreto necesitas !border-t-0.

/* divide-y en 3.4.19 */
.divide-y > :not([hidden]) ~ :not([hidden]) { border-top-width: 1px; }

/* divide-y en 4.3.3 (simplificado) */
:where(.divide-y > :not(:last-child)) { border-bottom-width: 1px; }

El cambio responde a un problema de rendimiento en páginas grandes, según la guía oficial. Tiene tres consecuencias prácticas, que hemos medido con el mismo HTML en las dos versiones.

1. El último hijo oculto deja una línea colgando. En v4, :last-child selecciona al último hijo del HTML, esté visible o no. Si ocultas el último con el atributo hidden o con la clase hidden, el anterior conserva su línea inferior:

<ul class="divide-y divide-rose-400">
  <li>Primero</li>
  <li>Segundo</li>
  <li hidden>Tercero</li>
</ul>
Segundo <li>:  v3.4.19  border-top: 1px   border-bottom: 0     correcto
               v4.3.3   border-top: 0     border-bottom: 1px   línea sobrante bajo el último visible

Tienes dos soluciones. La primera, y más limpia, es no pintar el elemento: si usas un framework, no lo incluyas en el HTML. La segunda, cuando lo ocultas con JavaScript, es copiar con una variante arbitraria el selector de v3, que funciona en las dos versiones:

<ul class="[&>:not([hidden])~:not([hidden])]:border-t [&>:not([hidden])~:not([hidden])]:border-rose-400">
  <li>Primero</li>
  <li>Segundo</li>
  <li hidden>Tercero</li>
</ul>

2. Las clases del hijo ahora ganan. :where() tiene especificidad cero, de modo que en v4 una clase en el hijo se impone al divisor del padre. Esto es una mejora:

<ul class="divide-y-4 divide-red-500">
  <li class="border-b-0">Sin línea debajo en v4</li>
  <li class="border-blue-500">Línea azul en v4</li>
  <li>Último</li>
</ul>

3. Si hay márgenes en los hijos, la línea cambia de sitio. Antes iba encima de cada hijo, y ahora va debajo. Una tarjeta con mt-4 puede quedar con la línea a otra distancia. Si tu diseño dependía de ello, usa flex flex-col gap-4 y bordes explícitos.


9. Sombras: shadow-*

Una sombra sugiere que un elemento flota sobre el fondo. En una interfaz, más sombra significa más altura: las tarjetas llevan poca, y los menús y los modales, mucha.

Muestrario de tamaños de sombra, sombras de color y sombras interiores

Figura 7. Escala de sombras de v4, color y opacidad, y capas interiores.

La escala de sombras 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), shadow-sm es la sombra más pequeña (0 1px 2px); en v4 esa sombra se llama shadow-xs, y shadow-sm da la que en v3 era shadow. Si usas la LTS: usa shadow-sm y shadow en lugar de shadow-xs y shadow-sm. Para shadow-2xs, escribe shadow-[0_1px_rgb(0_0_0/0.05)].

Clase en v4 Valor Clase en v3
shadow-2xs 0 1px rgb(0 0 0 / 0.05) shadow-[0_1px_rgb(0_0_0/0.05)]
shadow-xs 0 1px 2px 0 rgb(0 0 0 / 0.05) shadow-sm
shadow-sm 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1) shadow
shadow-md 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1) shadow-md
shadow-lg 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1) shadow-lg
shadow-xl 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1) shadow-xl
shadow-2xl 0 25px 50px -12px rgb(0 0 0 / 0.25) shadow-2xl
shadow-none Sin sombra shadow-none

Como pasa con rounded, la clase shadow sin sufijo sigue funcionando en v4 y da lo mismo que en v3, pero es obsoleta. El falso amigo es shadow-sm, no shadow.

Fíjate en que los valores tienen dos sombras: una amplia y otra corta, con extensión negativa. Es lo que hace que parezcan naturales: la luz real genera una penumbra difusa y una sombra de contacto más nítida.

Color de la sombra

El color se da con una clase aparte, que se combina con la del tamaño:

<div class="shadow-lg shadow-indigo-500">Sombra de color</div>
<div class="shadow-lg shadow-indigo-500/50">Con el 50 % de opacidad</div>
<button class="rounded-lg bg-indigo-600 p-4 text-white shadow-xl shadow-indigo-600/40">Botón con resplandor</button>

Una sombra de color usa todo el color indicado, sin el 10 % de las sombras negras. Por eso suele quedar demasiado fuerte y conviene rebajarla con /40 o /50.

Opacidad en la clase de tamaño: shadow-lg/50 🆕 v4.1

🆕 Desde Tailwind 4.1. No existe en la LTS (Tailwind 3.4). Si usas la LTS: combina el tamaño con un color: shadow-lg shadow-black/50 da exactamente el mismo resultado que shadow-lg/50.

Desde v4.1 puedes dar la opacidad en la propia clase de tamaño. Hay que tener cuidado con lo que significa el número, porque no es una reducción: sustituye a la opacidad de la sombra. Con un 10 % de partida, /30 da una sombra más oscura que la original. Puedes verlo en la Figura 7: es la última tarjeta de la fila de color.

Medido en Chromium (primera capa de la sombra):
shadow-xl                          rgba(0, 0, 0, 0.1)    <- 10 % del tema
shadow-xl/5                        oklab(0 0 0 / 0.05)   <- más suave
shadow-xl/30                       oklab(0 0 0 / 0.3)    <- MÁS oscura que shadow-xl
shadow-xl shadow-black/30          oklab(0 0 0 / 0.3)    <- lo mismo, con otra escritura
shadow-xl/30 shadow-indigo-500/50  opacidad 0,15         <- con color, los dos valores se multiplican

Sin color propio, el número fija la opacidad final. Con un color que ya trae opacidad, los dos valores se multiplican.

Quitar o heredar una sombra

shadow-none solo limpia su capa: no afecta a un anillo que tengas puesto. shadow-inherit, por su parte, hereda el color de sombra del padre.

Clases: ring-2 shadow-none
Medido: el anillo sigue ahí; solo la capa de sombra queda vacía

10. Capas interiores 🆕 v4.0

🆕 Desde Tailwind 4.0. No existe en la LTS (Tailwind 3.4). Si usas la LTS: usa shadow-inner para una sombra interior, y ring-1 ring-inset para un anillo interior. Si necesitas combinarlos con una sombra exterior, escríbelos juntos en un valor arbitrario, como shadow-[inset_0_2px_4px_rgb(0_0_0/0.05),0_10px_15px_-3px_rgb(0_0_0/0.1)].

v4 añade dos familias de utilidades que se dibujan hacia dentro y que no interfieren con las sombras ni con los anillos exteriores.

Sombras interiores: inset-shadow-*

Clase Valor
inset-shadow-2xs inset 0 1px rgb(0 0 0 / 0.05)
inset-shadow-xs inset 0 1px 1px rgb(0 0 0 / 0.05)
inset-shadow-sm inset 0 2px 4px rgb(0 0 0 / 0.05)
inset-shadow-none Sin sombra interior

Aceptan color (inset-shadow-indigo-500/60). El modificador de opacidad en el tamaño (inset-shadow-sm/50) llegó en v4.1, igual que en shadow-*.

Anillos interiores: inset-ring-*

inset-ring mide 1 px y usa el color del texto; inset-ring-2 inset-ring-indigo-500/40 da otro grosor y color. ¿En qué se diferencia de ring-inset? ring-inset convierte en interior el único anillo que hay. inset-ring es otro anillo: puedes tener uno por dentro y otro por fuera a la vez.

Por qué existen: cinco capas en una propiedad 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), box-shadow compone tres capas: ring-offset, ring y shadow, sin capas interiores propias. Si usas la LTS: para combinar más de lo que permiten esas tres capas, junta todas las sombras en un solo valor arbitrario shadow-[...], separadas por comas.

En CSS solo existe una propiedad box-shadow. Si dos clases la escribieran directamente, la última borraría a la otra. Por eso Tailwind guarda cada capa en una variable y compone el valor final:

/* Lo que escribe shadow-lg en 4.3.3 */
.shadow-lg {
  --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}

Cinco variables de Tailwind 4 que se combinan en box-shadow, frente a las tres de Tailwind 3

Figura 8. Cada familia de clases rellena su propia variable, y todas confluyen en box-shadow.

Tres reglas salen de este diagrama:

  1. Clases de capas distintas se suman. ring-2 shadow-lg da anillo y sombra.
  2. Clases de la misma capa se pisan, y gana la que aparece después en el CSS, no en tu atributo class. shadow-lg y shadow-inner escriben --tw-shadow, así que solo una sobrevive.
  3. Un valor arbitrario entra por la capa de sombra, salvo que escribas la propiedad. shadow-[0_0_0_3px_red] convive con un anillo. En cambio, [box-shadow:0_0_0_3px_red] sobrescribe la propiedad entera y borra todas las capas.

La segunda regla la medimos con los dos órdenes, y el resultado es idéntico:

class="shadow-lg shadow-inner"     ->  solo la sombra de shadow-lg
class="shadow-inner shadow-lg"     ->  solo la sombra de shadow-lg
class="shadow-lg inset-shadow-sm"  ->  las dos (solo en v4)

Con estas capas se construye un botón con relieve, como en la Figura 9:

Un botón al que se añaden capas de sombra y anillos paso a paso, y prueba de que dos clases de la misma capa se pisan

Figura 9. Cada clase añade una capa al mismo box-shadow, sin borrar las anteriores.

<button class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white
               shadow-md
               ring-1 ring-indigo-900
               inset-ring inset-ring-white/20
               inset-shadow-sm inset-shadow-white/40">
  Comprar
</button>

11. Sombras en modo oscuro

Una sombra negra sobre fondo casi negro no se ve. Si tu interfaz tiene modo oscuro, la sensación de altura debe venir de otra parte. Hay varias técnicas, recogidas en la Figura 10, capturada con el modo oscuro del sistema activo.

Seis tarjetas en modo oscuro: solo sombra, con anillo, con brillo superior, sombra de color, borde y fondo más claro

Figura 10. Formas de separar una tarjeta del fondo en modo oscuro. La primera, solo con sombra, apenas se distingue.

Técnica Clases Cuándo
Anillo claro y casi transparente dark:ring-1 dark:ring-white/10 La opción más limpia y habitual
Borde de un gris más claro dark:border-slate-700 Si el diseño ya usa bordes
Superficie más clara dark:bg-slate-800 Para elementos que deben parecer más cercanos
Sombra de color shadow-lg shadow-indigo-500/30 Resplandor para destacar un elemento
<article class="rounded-xl bg-white p-5 shadow-lg ring-1 ring-black/5 dark:bg-slate-900 dark:ring-white/10">
  <h3 class="font-semibold">Con anillo</h3>
  <p class="text-sm">En claro, sombra y un perfilado sutil. En oscuro, el anillo dibuja el contorno.</p>
</article>

El patrón ring-1 ring-black/5 usado en modo claro es muy común: añade un perfilado casi invisible que mejora la definición de la tarjeta, y al usar opacidad se adapta a cualquier color de fondo. La variante dark: se explica en el módulo 12.

Brillo superior 🆕 v4.0

🆕 Desde Tailwind 4.0. No existe en la LTS (Tailwind 3.4). Si usas la LTS: consigue el mismo brillo con dark:shadow-[inset_0_1px_rgb(255_255_255/0.2)], teniendo en cuenta que ocupa la capa de sombra.

Una línea clara arriba simula luz que cae desde encima:

<article class="rounded-xl bg-white p-5 shadow-lg dark:bg-slate-900 dark:inset-shadow-2xs dark:inset-shadow-white/20">
  Con brillo superior
</article>

12. Opacidad: opacity-*

opacity-* vuelve transparente el elemento completo, con su texto, sus bordes y todos sus hijos. Es distinto del modificador de color, que solo afecta a esa propiedad.

Tres tarjetas sobre una imagen comparando opacidad completa y fondo transparente, seguidas de ejemplos de mezcla

Figura 11. Arriba, opacity-50 frente a bg-white/50. Debajo, modos de mezcla, isolate y bg-blend-*.

<div class="bg-white p-4 opacity-50">El texto también queda a la mitad y cuesta leerlo</div>
<div class="bg-white/50 p-4">Solo el fondo es transparente: el texto se lee perfectamente</div>
Quieres… Usa
Un fondo translúcido con texto legible bg-white/50
Atenuar un elemento entero, como un botón desactivado opacity-50
Mostrar u ocultar con transición opacity-0 y opacity-100

La escala va de 5 en 5, de opacity-0 a opacity-100.

⚠️ Importante: opacity-0 no quita el elemento: sigue ocupando sitio, recibiendo clics y siendo enfocable con el teclado. Para ocultarlo de verdad, añade invisible, o hidden si tampoco debe ocupar sitio. Las veremos en el módulo 07.

Un botón desactivado (disabled:opacity-50) es el uso típico. Vigila el contraste: el texto al 50 % de opacidad puede quedar por debajo de lo legible.


13. Modos de mezcla: mix-blend-*, bg-blend-* e isolate

Un modo de mezcla decide cómo se combinan los colores de un elemento con los de lo que tiene detrás. Son las mismas fórmulas que en un programa de edición de imágenes. No son exclusivas de Tailwind: mix-blend-multiply es solo mix-blend-mode: multiply.

Clase Efecto Uso típico
mix-blend-multiply Oscurece: multiplica los colores Quitar el fondo blanco de un logotipo sobre una foto
mix-blend-screen Aclara: lo contrario que multiply Luces y brillos
mix-blend-overlay Refuerza el contraste Tintar imágenes
mix-blend-difference Resta los colores Texto que contrasta con cualquier fondo
mix-blend-normal Sin mezcla Valor por defecto

Existen también mix-blend-darken, -lighten, -color-dodge, -color-burn, -hard-light, -soft-light, -exclusion, -hue, -saturation, -color, -luminosity, -plus-lighter y -plus-darker.

<div class="relative h-28 overflow-hidden rounded-lg">
  <img src="/img/escena.svg" alt="" class="h-full w-full object-cover">
  <div class="absolute inset-0 flex items-center justify-center text-4xl font-black text-white mix-blend-difference">
    OFERTA
  </div>
</div>

Aquí el texto blanco se invierte con lo que tiene detrás, y por eso cambia de color letra a letra en la Figura 11.

Limitar la mezcla con isolate

mix-blend-* se mezcla con todo lo que queda debajo, incluido el fondo de la página. isolate crea un grupo aislado: los elementos de dentro solo se mezclan entre sí.

<div class="bg-[repeating-linear-gradient(45deg,#fde68a_0_14px,#a7f3d0_14px_28px)]">
  <div class="isolate flex">
    <div class="size-20 rounded-full bg-rose-500 mix-blend-multiply"></div>
    <div class="-ml-8 size-20 rounded-full bg-sky-500 mix-blend-multiply"></div>
  </div>
</div>

Sin isolate, las franjas se transparentan a través de los círculos. Con isolate, solo se oscurece la zona donde los círculos se cruzan. Compara las dos versiones en la Figura 11.

Mezclar capas del mismo fondo: bg-blend-*

bg-blend-* no mezcla el elemento con lo de detrás, sino las capas de fondo del propio elemento: su imagen con su color de fondo. Es una forma sencilla de teñir una imagen:

<div class="h-28 bg-indigo-500 bg-[url(/img/escena.svg)] bg-cover bg-center bg-blend-luminosity"></div>

14. Filtros: blur-*, grayscale y compañía

Los filtros procesan los píxeles ya dibujados de un elemento, con sus hijos incluidos. Funcionan sobre cualquier elemento, no solo sobre imágenes.

Doce filtros aplicados a la misma imagen, combinaciones y diferencia entre shadow y drop-shadow

Figura 12. Filtros sobre la misma imagen, combinaciones y, abajo, shadow-* frente a drop-shadow-*.

Clases CSS Observación
brightness-0…brightness-200 filter: brightness() Por debajo de 100 oscurece, y por encima aclara
contrast-0…contrast-200 filter: contrast() Por debajo de 100 apaga la imagen
grayscale, grayscale-0 filter: grayscale() Blanco y negro, o color normal
sepia, sepia-0 filter: sepia() Tono envejecido
invert, invert-0 filter: invert() Colores invertidos
saturate-0…saturate-200 filter: saturate() Intensidad del color
hue-rotate-15…hue-rotate-180, -hue-rotate-90 filter: hue-rotate() Gira el tono
filter-none filter: none Quita todos los filtros

Los filtros se combinan, pero en orden fijo

Como con las sombras, cada clase rellena una variable, y filter las compone todas. Por eso se suman sin pisarse:

<img src="/img/escena.svg" alt="" class="grayscale contrast-125">
<img src="/img/escena.svg" alt="" class="brightness-75 saturate-150">

Pero el orden de aplicación lo fija Tailwind, no tu atributo class:

filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,)
        var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);

Con los filtros el orden importa: girar el tono y luego aplicar sepia no da lo mismo que al revés. Si necesitas un orden concreto, escribe la propiedad completa:

<!-- Estas dos dan lo mismo: blur(12px) hue-rotate(90deg) sepia(1) -->
<div class="sepia hue-rotate-90 blur-md">Primero</div>
<div class="hue-rotate-90 blur-md sepia">Segundo</div>

<!-- Aquí mandas tú: sepia(1) hue-rotate(90deg) -->
<div class="[filter:sepia(1)_hue-rotate(90deg)]">Orden propio</div>

Desenfoque: la escala de blur-* 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), blur-sm desenfoca 4 px; en v4, 8 px, justo el doble. Si usas la LTS: usa blur-sm para 4 px y blur para 8 px. blur-xs no existe.

Clase en v4 Valor Clase en v3
blur-xs 4 px blur-sm
blur-sm 8 px blur

El resto de la escala coincide: blur-md (12 px), blur-lg (16 px), blur-xl (24 px), blur-2xl (40 px) y blur-3xl (64 px).

La clase blur sin sufijo sigue funcionando en v4 con los mismos 8 px, pero es obsoleta. El desenfoque también difumina los bordes de la imagen, que se abren más allá de su caja, como ves en blur-md en la Figura 12.

shadow-* frente a drop-shadow-*

shadow-* sigue siempre la caja rectangular. drop-shadow-* es un filtro que analiza los píxeles reales, así que sigue la forma de lo que se dibuja, respetando la transparencia. Es la diferencia entre las dos primeras imágenes de la última fila de la Figura 12: con shadow-xl aparece un rectángulo; con drop-shadow-xl, la silueta del portátil.

Escala de drop-shadow-* 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), drop-shadow-md, -lg y -xl usan dos sombras y dan otro resultado que en v4, donde usan una sola, y además drop-shadow-sm pasa a llamarse drop-shadow-xs. Si usas la LTS: las clases de la tabla de esta sección dan los valores de v3; para el aspecto exacto de v4 usa un valor arbitrario, como drop-shadow-[0_9px_7px_rgb(0_0_0/0.1)].

Esta es la fila de la tabla de renombrados que más engaña. La guía oficial solo cita drop-shadow-sm y drop-shadow, pero md, lg y xl conservan el nombre y cambian de valor. El registro de cambios lo recoge como “use single drop shadow values”:

Clase v3.4.19 v4.3.3
drop-shadow-xs No existe 0 1px 1px rgb(0 0 0 / 0.05)
drop-shadow-sm 0 1px 1px rgb(0 0 0 / 0.05) 0 1px 2px rgb(0 0 0 / 0.15)
drop-shadow 0 1px 2px al 10 % y 0 1px 1px al 6 % Igual que en v3, obsoleta
drop-shadow-md 0 4px 3px al 7 % y 0 2px 2px al 6 % 0 3px 3px rgb(0 0 0 / 0.12)
drop-shadow-lg 0 10px 8px al 4 % y 0 4px 3px al 10 % 0 4px 4px rgb(0 0 0 / 0.15)
drop-shadow-xl 0 20px 13px al 3 % y 0 8px 5px al 8 % 0 9px 7px rgb(0 0 0 / 0.1)
drop-shadow-2xl 0 25px 25px rgb(0 0 0 / 0.15) 0 25px 25px rgb(0 0 0 / 0.15)

El cambio de drop-shadow-sm a drop-shadow tampoco es un simple renombrado: el drop-shadow de v3 tiene dos capas y el drop-shadow-sm de v4, una.

Sombras de filtro con color: drop-shadow-<color> 🆕 v4.1

🆕 Desde Tailwind 4.1. No existe en la LTS (Tailwind 3.4). Si usas la LTS: escribe el valor completo en un valor arbitrario, como drop-shadow-[0_4px_4px_rgb(99_102_241/0.6)], que funciona en las dos versiones.

Igual que shadow-*, ahora drop-shadow-* acepta color y opacidad:

<img src="/img/portatil.svg" alt="" class="drop-shadow-xl drop-shadow-indigo-500">
<img src="/img/portatil.svg" alt="" class="drop-shadow-xl drop-shadow-rose-500/60">
<img src="/img/portatil.svg" alt="" class="drop-shadow-xl/30">

Funcionan como en shadow-*: drop-shadow-xl/30 da una sombra más oscura, porque sustituye la opacidad del 10 %.


15. Filtros del fondo: backdrop-*

blur-md desenfoca el propio elemento. backdrop-blur-md desenfoca lo que hay detrás y deja el contenido nítido. Sin él no se puede hacer el efecto de cristal esmerilado de muchas interfaces modernas.

Barra de navegación y tarjetas de cristal sobre una imagen, muestra de filtros de fondo y efecto de un padre con opacidad

Figura 13. Arriba, cristal esmerilado con distintos desenfoques. En el centro, otros filtros de fondo. Abajo, un padre con opacidad anula el filtro del hijo.

Para que funcione, el elemento necesita un fondo translúcido o transparente: si es opaco, no deja ver nada detrás.

<header class="sticky top-0 border-b border-white/30 bg-white/30 px-5 py-3 backdrop-blur-md">
  <strong>Tienda EMPRESA</strong>
</header>

Las clases son las mismas que las de los filtros normales, con el prefijo backdrop-. backdrop-blur-md, backdrop-brightness-50, backdrop-contrast-50, backdrop-grayscale, backdrop-hue-rotate-90, backdrop-invert, backdrop-saturate-150 y backdrop-sepia actúan sobre lo que hay detrás. backdrop-opacity-50 reduce la intensidad de los demás filtros de fondo.

💡 Consejo: Un cristal resulta más vivo si añades backdrop-saturate-150: el desenfoque apaga los colores, y la saturación los recupera. Es lo que hace la tercera tarjeta de la Figura 13.

Escala de backdrop-blur-* 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), backdrop-blur-sm desenfoca 4 px; en v4, 8 px. Si usas la LTS: usa backdrop-blur-sm para 4 px y backdrop-blur para 8 px. backdrop-blur-xs no existe.

La escala es la misma que la de blur-*, y backdrop-blur sin sufijo sigue en v4 con 8 px, pero obsoleta.

Un padre puede anular el filtro de su hijo

Es el fallo más desconcertante de este módulo: el backdrop-* “no hace nada” y el CSS parece correcto.

📘 Concepto: backdrop-filter no ve toda la página, sino lo que hay hasta su raíz de fondo (backdrop root), que es el antepasado más cercano con opacity menor que 100 %, filter, mask, mix-blend-mode, backdrop-filter u otras propiedades similares. Si un antepasado tiene una de ellas, el hijo solo filtra lo que hay dentro de ese antepasado.

En la Figura 13, abajo, los dos paneles son iguales salvo por una clase. A la derecha, el padre tiene opacity-95, y backdrop-grayscale se queda sin efecto, porque dentro del padre no hay nada detrás de él:

<!-- El hijo filtra la imagen -->
<div class="bg-[url(/img/escena.svg)] bg-cover">
  <div>
    <div class="h-20 backdrop-grayscale">Funciona</div>
  </div>
</div>

<!-- El padre es una raíz de fondo: el hijo no alcanza la imagen -->
<div class="bg-[url(/img/escena.svg)] bg-cover">
  <div class="opacity-95">
    <div class="h-20 backdrop-grayscale">No filtra la imagen</div>
  </div>
</div>

Lo mismo ocurre al anidar dos paneles de cristal. La solución es sacar el efecto del padre: pasa la opacidad a un color (bg-white/90) o haz que el panel con backdrop-* no sea descendiente del que tiene el efecto.

Rendimiento y accesibilidad

backdrop-blur-* es de los efectos más caros de dibujar, porque el navegador debe volver a procesar lo que hay detrás en cada frame cuando algo se mueve. Una barra o un modal están bien; decenas de tarjetas con desenfoque en una lista larga pueden ir lentas en un móvil modesto.

Además, un fondo translúcido reduce el contraste del texto, y algunas personas configuran su sistema para reducir transparencias. Puedes respetar esa preferencia con una variante arbitraria:

<header class="bg-white/50 backdrop-blur-md [@media(prefers-reduced-transparency:reduce)]:bg-white">
  Tienda EMPRESA
</header>

Lo probamos en Chromium emulando esa preferencia: el fondo pasa de translúcido a rgb(255, 255, 255).


16. Efectos secundarios: apilamiento y position: fixed

Las clases de este módulo parecen puramente decorativas, pero algunas cambian cómo se organizan los elementos. Conocer esto te evitará horas de depuración.

Contextos de apilamiento

📘 Concepto: Un contexto de apilamiento es un grupo de elementos que se ordena como un bloque. El z-index de un hijo solo compite con sus hermanos de dentro del grupo. Por muy alto que sea, nunca pasará por encima de algo que esté por encima del grupo entero.

Lo hemos probado con el mismo montaje: un hijo con z-50 dentro de un padre, frente a una caja hermana del padre con z-10. Sin contexto, gana z-50. Con contexto, el hijo queda atrapado y gana z-10.

<div class="relative h-20">
  <div class="opacity-95">                                <!-- crea un contexto -->
    <div class="absolute inset-0 z-50 bg-rose-300">z-50</div>
  </div>
  <div class="relative z-10 h-20 bg-sky-300">z-10</div>     <!-- esta queda encima -->
</div>
Clase en el padre ¿Crea contexto? Gana
Ninguna No z-50
shadow-lg, zoom-125 No z-50
opacity-95 (cualquier valor menor que 100), isolate Sí z-10
grayscale y demás filtros, backdrop-blur-sm Sí z-10
mix-blend-multiply, máscaras mask-* Sí z-10

Es muy típico en este caso: un menú desplegable con z-50 queda oculto bajo el contenido, porque una tarjeta antepasada tiene opacity-95 o un filtro. El estudio completo de z-index está en el módulo 07.

💡 Consejo: isolate es la forma limpia de crear un contexto a propósito. Evita usar una opacidad casi completa como truco, porque esconde la intención y crea además una raíz de fondo.

filter atrapa a position: fixed

Un elemento fixed se coloca respecto a la ventana… salvo que un antepasado tenga filter o backdrop-filter. En ese caso, se coloca respecto a ese antepasado.

Elemento fixed top-0 left-0 w-full, en una ventana de 900 px, dentro de un padre de 256 px:

Clase en el padre      x     ancho    Resultado (igual en v3.4.19 y en v4.3.3)
(ninguna)              0     900      Respecto a la ventana
opacity-50             0     900      Respecto a la ventana
blur-none              0     900      Respecto a la ventana: el filtro queda vacío
blur-sm                112   256      Atrapado en el padre
backdrop-blur-sm       112   256      Atrapado en el padre

Este es el motivo por el que un modal fixed inset-0 dentro de una tarjeta con backdrop-blur no cubre la pantalla. La solución es colocar el modal fuera de ese antepasado, justo antes de cerrar <body>, o usar el elemento <dialog>, que se muestra en la capa superior del navegador (lo verás en el módulo 17).

Fíjate también en que opacity crea contexto de apilamiento, pero no atrapa a fixed. Los tres fenómenos de este módulo se parecen, pero no son el mismo.

⚠️ Importante: En v3, drop-shadow-none genera un filtro de verdad, drop-shadow(0 0 #0000), que sigue creando contexto de apilamiento. En v4, drop-shadow-none deja filter: none. Para quitar filtros en v3, usa filter-none, que funciona en las dos versiones.


17. Máscaras: mask-* 🆕 v4.1

🆕 Desde Tailwind 4.1. No existe en la LTS (Tailwind 3.4). Si usas la LTS: escribe la propiedad con un valor arbitrario, como [mask-image:linear-gradient(to_bottom,black_30%,transparent_95%)], que da el mismo resultado que mask-b-from-30% mask-b-to-95%.

Una máscara decide qué partes de un elemento se ven: donde la máscara es opaca, el elemento se ve; donde es transparente, desaparece. Hasta v4.1 había que escribir degradados a mano, y ahora hay utilidades para casi todo. Son el equivalente CSS de las máscaras de capa de un editor de imágenes.

Muestrario de máscaras lineales por lado, control de from y to, radiales, cónicas y un caso real

Figura 14. Las zonas enmascaradas dejan ver el fondo gris. Abajo, una cabecera fundida con la página.

from y to

Las máscaras de degradado se describen con dos puntos: dónde empieza a desaparecer el elemento (from) y dónde ya no se ve (to).

Barra de degradado que muestra las posiciones from y to, e intersección de dos máscaras

Figura 15. from y to son las dos paradas del degradado. Varias máscaras se intersecan.

La clase mask-b-from-50% significa: desvanece hacia abajo, empezando en el 50 %. La letra indica el borde hacia el que desaparece, y el valor es la distancia desde el borde opuesto. Por defecto, from vale 0% y to vale 100%.

Clases Resultado
mask-b-from-50% Se ve hasta la mitad y se desvanece hasta el borde inferior
mask-b-to-60% Se desvanece desde arriba y desaparece en el 60 %
mask-b-from-30% mask-b-to-70% Desvanecimiento entre el 30 % y el 70 %
mask-b-from-12 mask-b-to-12 Con los dos iguales hay corte brusco: a 3 rem, porque usa la escala de espaciado

Por lados

Las letras son t, r, b y l para cada borde, x para los dos laterales e y para arriba y abajo: mask-x-from-70%, mask-y-to-90%.

El valor admite porcentajes (mask-b-from-50%), números de la escala de espaciado (mask-l-from-20, que da calc(var(--spacing) * 20)), y valores arbitrarios (mask-b-from-[3px]). Los colores (mask-t-from-black) cambian la opacidad de cada parada: solo cuenta su transparencia, salvo que uses mask-luminance.

Degradados lineales con ángulo, radiales y cónicos

<img src="/img/escena.svg" alt="" class="mask-linear-45 mask-linear-from-40%">
<img src="/img/escena.svg" alt="" class="mask-radial-from-40%">
<img src="/img/escena.svg" alt="" class="mask-circle mask-radial-at-right mask-radial-from-30% mask-radial-to-70%">
<img src="/img/escena.svg" alt="" class="mask-conic-from-75% mask-conic-to-75%">

Las familias son mask-linear-* con su ángulo (mask-linear-45, -mask-linear-45), mask-radial-* con su forma (mask-circle, mask-ellipse), tamaño (mask-radial-closest-side) y posición (mask-radial-at-right), y mask-conic-* con su ángulo (mask-conic-90).

Las máscaras cónicas giran como las agujas del reloj desde arriba, y son la base de indicadores circulares de progreso.

Combinar máscaras

Tailwind añade mask-composite: intersect a cada utilidad de degradado. Por eso, al combinar varias, solo queda lo que todas dejan ver. Es muy útil para desvanecer dos bordes a la vez:

<img src="/img/escena.svg" alt="" class="mask-r-from-60% mask-b-from-60%">

Máscaras con imagen

Una imagen con transparencia también sirve de máscara, y el elemento toma su silueta. El fondo gris de la Figura 14 se ve alrededor de la forma del portátil.

<img src="/img/escena.svg" alt="" class="mask-[url(/img/portatil.svg)] mask-contain mask-center mask-no-repeat">
Clases Propiedad Se parecen a…
mask-[url(...)], mask-none mask-image bg-[url(...)]
mask-contain, mask-cover, mask-auto mask-size bg-contain…
mask-center, mask-top, mask-top-left… mask-position bg-center…
mask-no-repeat, mask-repeat, mask-repeat-x… mask-repeat bg-no-repeat…
mask-add, mask-subtract, mask-intersect, mask-exclude mask-composite Cómo se combinan varias máscaras
mask-alpha, mask-luminance, mask-match mask-mode Si manda la transparencia o el brillo

Caso real: cabecera fundida con la página

<header class="relative h-72 overflow-hidden">
  <img src="/img/escena.svg" alt="" class="absolute inset-0 h-full w-full object-cover mask-b-from-30% mask-b-to-95%">
  <div class="absolute inset-x-0 bottom-4 text-center">
    <h1 class="text-3xl font-black">Rebajas de otoño</h1>
    <p class="text-slate-600">Hasta un 30 % en portátiles seleccionados</p>
  </div>
</header>

La imagen no se funde con el color de fondo de la página: se vuelve transparente. Por eso funcionará igual en modo oscuro, sin tener que cambiar ningún color. Un degradado de color puesto encima de la imagen, en cambio, tendría que repetir el color de fondo.


18. Escala de la caja: zoom-* 🆕 v4.3

🆕 Desde Tailwind 4.3. No existe en la LTS (Tailwind 3.4). Si usas la LTS: escribe la propiedad directamente con un valor arbitrario, por ejemplo [zoom:1.25], que da el mismo resultado que zoom-125.

zoom-* escribe la propiedad CSS zoom, que agranda o reduce un elemento recalculando el diseño, como si cada longitud se multiplicara por ese factor.

/* Lo que genera zoom-125 */
.zoom-125 {
  zoom: 125%;
}

Acepta cualquier número (zoom-50, zoom-110, zoom-200), valores arbitrarios (zoom-[1.5]) y variables (zoom-(--escala)). La propiedad funciona en todos los navegadores que admite v4; MDN la marca como disponible de forma general desde mayo de 2024, cuando la incorporó Firefox.

Tarjetas con distintos valores de zoom que se recolocan, frente a scale-150 que pisa a sus vecinos

Figura 16. zoom-* hace sitio; scale-* solo se dibuja más grande.

zoom-* frente a scale-*

Las dos clases parecen hacer lo mismo, pero scale es una transformación visual: el diseño no se entera, y el elemento pisa a sus vecinos. Lo medimos con una tarjeta de w-32 (128 px) seguida de un vecino a 16 px de distancia:

            Ancho dibujado   Ancho CSS   Vecino empieza en   ¿Pisa al vecino?
zoom-150    192 px           128 px      664 (a 16 px)       No
scale-150   192 px           128 px      312                 Sí: la tarjeta acaba en 328
  zoom-* scale-*
Recoloca a los vecinos Sí No
Coste Recalcula el diseño Muy bajo, solo se redibuja
Animación Poco fluida Fluida
Uso típico Hacer más densa o más grande una zona Efectos al pasar el ratón

Usa scale-* para animaciones, y lo estudiarás en el módulo 13. Usa zoom-* para cambiar de forma estable el tamaño de una zona, como un panel denso de datos o una vista previa.

⚠️ Importante: No uses zoom-* para que la interfaz “quepa” en un móvil: eso se resuelve con el diseño adaptable. Además, reducir la letra por debajo de lo legible perjudica a todos.


19. Escalas renombradas 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), las clases de shadow, drop-shadow, blur, backdrop-blur y rounded tienen nombres desplazados un puesto respecto a v4, y outline-none y ring cambian de significado. Si usas la LTS: usa la columna v3 de la tabla de esta sección.

En v3 las escalas tenían un valor “por defecto” sin nombre (shadow, blur, rounded), justo entre sm y md. v4 elimina ese hueco para que todos los valores tengan nombre, y la escala se corre un puesto.

Diagrama de la escala de radios en v3 y v4: rounded-sm pasa a rounded-xs y rounded pasa a rounded-sm

Figura 17. El valor no cambia; cambia el nombre. El peligro está en rounded-sm, que existe en las dos versiones.

v3.4 Se llama en v4 Valor Si no cambias nada al migrar…
shadow-sm shadow-xs 0 1px 2px 0 rgb(0 0 0 / 0.05) La sombra se hace mayor
shadow shadow-sm Dos capas, 0 1px 3px y 0 1px 2px -1px Se ve igual, pero es obsoleta
drop-shadow-sm drop-shadow-xs 0 1px 1px rgb(0 0 0 / 0.05) La sombra se hace mayor
drop-shadow drop-shadow-sm Parecido, no idéntico: ver la sección 14 Se ve igual, pero es obsoleta
blur-sm blur-xs 4 px El desenfoque se duplica
blur blur-sm 8 px Se ve igual, pero es obsoleta
backdrop-blur-sm backdrop-blur-xs 4 px El desenfoque se duplica
backdrop-blur backdrop-blur-sm 8 px Se ve igual, pero es obsoleta
rounded-sm rounded-xs 2 px La esquina se duplica
rounded rounded-sm 4 px Se ve igual, pero es obsoleta
outline-none outline-hidden Contorno transparente Pierde el foco en colores forzados
ring ring-3 3 px Pasa a 1 px y al color del texto

La guía de actualización presenta shadow, blur y rounded como renombradas, y la columna final puede sorprender: no desaparecen. Las hemos compilado y dan el mismo valor que en v3, porque theme.css las conserva en un bloque Deprecated. El problema real son las clases -sm, que siguen existiendo y cambian de valor.

La Figura 18 muestra las mismas clases compiladas con cada versión. Es el mismo HTML, y las dos columnas no tienen ni una sola clase distinta:

El mismo HTML compilado con Tailwind 3.4.19 y con Tailwind 4.3.3, con diferencias en borde, anillo, contorno y desenfoque

Figura 18. Mismo código, dos versiones reales. Mira el color del borde, el grosor de ring y outline, outline-2 (invisible en v3), blur-sm y backdrop-blur-sm.

⚠️ Importante: Una búsqueda y sustitución ingenua estropea el proyecto. Si primero cambias shadow por shadow-sm y luego shadow-sm por shadow-xs, todas acaban en shadow-xs. Hay que hacerlo en el orden inverso, o usar npx @tailwindcss/upgrade, como verás en el módulo 19.


20. Valores propios y escalas dinámicas

Cuando la escala no te sirve, tienes varias salidas. Elige la primera que resuelva tu caso.

Valores arbitrarios

Funcionan igual en las dos versiones, con corchetes. En una sombra, los espacios se escriben con _:

<div class="border-[3px] border-[#123456]">Ancho y color exactos</div>
<div class="rounded-[1.25rem] shadow-[0_0_0_3px_rgb(225_29_72)]">Radio y sombra exactos</div>
<div class="opacity-[.67] blur-[2px]">Opacidad y desenfoque exactos</div>

Variables con paréntesis 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), las variables CSS se escriben con corchetes y var(), como rounded-[var(--radio)]; la sintaxis con paréntesis de v4 no existe. Si usas la LTS: usa corchetes y añade la pista de tipo cuando la clase sea ambigua, como border-[length:var(--grosor)] o shadow-[shadow:var(--sombra)].

v4 v3 Medido en Chromium
rounded-(--radio) rounded-[var(--radio)] Las dos dan el radio
border-(--color) border-[color:var(--color)] Color del borde
border-(length:--grosor) border-[length:var(--grosor)] Ancho del borde
shadow-(--sombra) shadow-[shadow:var(--sombra)] La sombra de la variable

⚠️ Importante: border puede significar ancho o color, y Tailwind no puede leer el valor de la variable. Por eso hay que indicar el tipo. En v3 el caso es aún más traicionero: shadow-[var(--sombra)] se interpreta como color de sombra, y lo medimos: no aparece ninguna sombra. Hay que escribir shadow-[shadow:var(--sombra)].

Escalas dinámicas 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), solo existen los valores fijos de cada escala; border-3 y opacity-33 no generan CSS. Si usas la LTS: escribe border-[3px], opacity-[.33], grayscale-[50%], brightness-[.37] o sepia-[.5].

v4 acepta cualquier número en estas familias, sin corchetes y sin configuración:

Familia Ejemplo en v4 Equivalente en v3
Anchos border-3, border-13, ring-13, outline-3 border-[3px], ring-[13px], outline outline-[3px]
Opacidad opacity-33 opacity-[.33]
Filtros brightness-37, contrast-37, saturate-37 brightness-[.37]
Filtros en porcentaje grayscale-50, sepia-50, invert-20 grayscale-[50%], sepia-[.5], invert-[.2]

Tokens propios en el tema 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), el tema se amplía en tailwind.config.js, no con @theme. Si usas la LTS: añade tus valores en theme.extend, dentro de borderRadius, boxShadow y blur.

Si un valor se repite, dale nombre. En v4 cada variable del tema crea sus clases:

@import "tailwindcss";

@theme {
  --radius-tarjeta: 1.25rem;
  --shadow-tarjeta: 0 8px 24px -6px rgb(30 58 138 / 0.25);
  --blur-suave: 2px;
}
<article class="rounded-tarjeta shadow-tarjeta">Tarjeta de la tienda</article>
<img src="/img/escena.svg" alt="" class="blur-suave">

El equivalente en v3, que hemos compilado y genera el mismo resultado:

// tailwind.config.js
export default {
  content: ['./index.html'],
  theme: {
    extend: {
      borderRadius: { tarjeta: '1.25rem' },
      boxShadow: { tarjeta: '0 8px 24px -6px rgb(30 58 138 / 0.25)' },
      blur: { suave: '2px' },
    },
  },
};

El tema se estudia en profundidad en el módulo 14.


21. Caso completo: tarjetas y pedidos de la tienda 🆕 v4.1

🆕 Desde Tailwind 4.1. No existe en la LTS (Tailwind 3.4). Si usas la LTS: en esta tarjeta de ejemplo, cambia drop-shadow-xl drop-shadow-black/40 por drop-shadow-[0_9px_7px_rgb(0_0_0/0.4)], inset-ring inset-ring-emerald-600/30 por ring-1 ring-inset ring-emerald-600/30, shadow-xs por shadow-sm, y la sombra del botón por shadow-[inset_0_1px_rgb(255_255_255/0.3),0_1px_3px_0_rgb(0_0_0/0.1)].

Reunimos casi todo lo visto con datos reales de la tienda de EMPRESA: tres productos en distintas situaciones de existencias y cuatro pedidos, uno por cada estado. La estructura de la tarjeta normal es la que construirás en el ejercicio 2, así que aquí vemos sus clases más ricas y la tarjeta agotada completa. Hemos compilado esta página con v4 y su traducción a v3 con los cambios del recuadro, y el resultado es visualmente igual.

Tres tarjetas de producto, una de ellas agotada, y una lista de últimos pedidos con insignias de estado

Figura 19. Bordes, radios, anillos, sombras por capas, filtros, backdrop-blur, opacidad y separadores trabajando juntos.

<!-- En la tarjeta normal, la imagen y el botón llevan varias capas -->
<img src="/img/portatil.svg" alt="" class="absolute inset-0 m-auto h-28 drop-shadow-xl drop-shadow-black/40">
<button type="button" class="rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm ring-1 ring-indigo-800 inset-shadow-2xs inset-shadow-white/30 hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
  Añadir
</button>

<!-- Tarjeta agotada: gris en la imagen, opacidad solo en el texto -->
<article class="overflow-hidden rounded-2xl bg-white shadow-xs ring-1 ring-slate-900/5">
  <div class="relative h-40 bg-[url(/img/escena.svg)] bg-cover bg-center grayscale">
    <img src="/img/teclado.svg" alt="" class="absolute inset-0 m-auto h-28 opacity-60">
    <span class="absolute inset-0 flex items-center justify-center bg-white/40 text-sm font-bold tracking-widest text-slate-800 uppercase">
      Agotado
    </span>
  </div>
  <div class="p-4 opacity-60">
    <h3 class="font-semibold">Pack teclado y ratón</h3>
    <p class="text-sm text-slate-500">Conjunto inalámbrico de teclado y ratón</p>
    <div class="mt-3 flex items-center justify-between">
      <span class="text-lg font-bold">34,50 €</span>
      <button type="button" disabled class="rounded-lg border border-dashed border-slate-400 px-3 py-1.5 text-sm font-semibold text-slate-500">
        Avisarme
      </button>
    </div>
  </div>
</article>

La lista de pedidos combina los separadores de la sección 8 con border-s-4 border-s-emerald-500 para la marca de color, y con las insignias del ejercicio 1.

Algunas decisiones que conviene comentar:

  • overflow-hidden en la tarjeta: recorta la imagen con el radio. Por eso los anillos y las sombras se ponen en la propia tarjeta, no en un hijo, que quedaría recortado.
  • ring-1 ring-slate-900/5 y no border: perfila sin ocupar sitio, y su opacidad se adapta al fondo.
  • drop-shadow-* en el portátil: la imagen es irregular; shadow-* dibujaría un rectángulo.
  • Opacidad por partes en la tarjeta agotada: no la aplicamos a la tarjeta entera, porque crearía un contexto de apilamiento y una raíz de fondo, y también apagaría el rótulo “Agotado”, que debe leerse bien.
  • inset-ring en las insignias: el fondo es muy claro y el anillo interior define el contorno sin cambiar el tamaño.
  • border-s-4: es lógico, de modo que con dir="rtl" la marca de color pasará al lado derecho sin tocar nada.
  • Sin anillo en el foco del botón: usa outline, que aguanta los colores forzados y no choca con el ring-1 que ya tiene.

22. Depuración de bordes y efectos

Síntoma Causa Solución
Copio border-2 y sale del color del texto Color por defecto de v4 Añade border-slate-200
outline-2 no se ve Estás en v3 Escribe outline outline-2
ring se ve fino y negro v4: 1 px y currentColor Usa ring-3 ring-blue-500
Las esquinas de la imagen asoman rounded-* no recorta a los hijos overflow-hidden en el padre
El anillo o la sombra se cortan Un antepasado tiene overflow-hidden Quita ese overflow, añade p-1, o usa ring-inset
La tabla no se redondea border-collapse: collapse de Preflight Mete la tabla en un contenedor redondeado
shadow-inner no se suma a shadow-lg Las dos escriben --tw-shadow En v4, inset-shadow-sm; en v3, un valor shadow-[...]
shadow-lg/30 queda más oscura El número sustituye al 10 % original Usa un valor menor, como shadow-lg/5
El anillo blanco aparece en el modo oscuro ring-offset pinta un color ring-offset-slate-900 o outline-offset-*
Una línea sobra al final de la lista Último hijo oculto con divide-* en v4 No lo pintes, o usa [&>:not([hidden])~:not([hidden])]:border-t
El foco no se ve en alto contraste outline-none más ring outline-hidden, o mejor focus-visible:outline-2
El desenfoque es el doble que en el diseño blur-sm vale 8 px en v4 Usa blur-xs
backdrop-blur-* no hace nada Fondo opaco, o antepasado con opacity o filter Usa bg-white/30 o retira el efecto del antepasado
z-50 no sube Un antepasado crea contexto de apilamiento Mueve el elemento o reorganiza los contextos
Un modal fixed no cubre la pantalla Un antepasado tiene filter o backdrop-filter Saca el modal de ahí o usa <dialog>
mix-blend-* se mezcla con el fondo de la página Es el comportamiento normal Pon isolate en un contenedor
border-(--x) da color y querías ancho Variable ambigua border-(length:--x)
shadow-[var(--x)] no da sombra en v3 Se interpreta como color shadow-[shadow:var(--x)]
Un HTML nuevo no recibe sus clases con Vite en marcha El plugin escaneó los ficheros al arrancar Reinicia Vite o guarda app.css para forzar otro escaneo

💡 Consejo: Ante un efecto que no funciona, abre las herramientas de desarrollo y mira Computed, no Styles. Ahí ves el box-shadow o el filter final, con todas las capas ya resueltas. Si la clase no aparece en Styles, Tailwind no la generó: vuelve a la depuración del módulo 01.


23. Ejercicios

Ejercicio 1: Insignias de estado de los pedidos 🆕 v4.0

🆕 Desde Tailwind 4.0. No existe en la LTS (Tailwind 3.4). Si usas la LTS: haz estas insignias con ring-1 ring-inset ring-emerald-600/30 en lugar de inset-ring inset-ring-emerald-600/30.

Los pedidos de EMPRESA pueden estar en cuatro estados: ENTREGADO, ENVIADO, PENDIENTE y CANCELADO. Crea una insignia con forma de píldora para cada uno, con fondo muy claro, texto oscuro, un punto de color delante del texto y un anillo interior semitransparente.

Ver solución
<ul class="flex flex-wrap gap-3">
  <li><span class="inline-flex items-center gap-1.5 rounded-full bg-emerald-50 px-2.5 py-0.5 text-xs font-medium text-emerald-700 inset-ring inset-ring-emerald-600/30"><span class="size-1.5 rounded-full bg-emerald-500"></span>ENTREGADO</span></li>
  <li><span class="inline-flex items-center gap-1.5 rounded-full bg-sky-50 px-2.5 py-0.5 text-xs font-medium text-sky-700 inset-ring inset-ring-sky-600/30"><span class="size-1.5 rounded-full bg-sky-500"></span>ENVIADO</span></li>
  <li><span class="inline-flex items-center gap-1.5 rounded-full bg-amber-50 px-2.5 py-0.5 text-xs font-medium text-amber-700 inset-ring inset-ring-amber-600/30"><span class="size-1.5 rounded-full bg-amber-500"></span>PENDIENTE</span></li>
  <li><span class="inline-flex items-center gap-1.5 rounded-full bg-rose-50 px-2.5 py-0.5 text-xs font-medium text-rose-700 inset-ring inset-ring-rose-600/30"><span class="size-1.5 rounded-full bg-rose-500"></span>CANCELADO</span></li>
</ul>

rounded-full da la píldora, y size-1.5 con rounded-full, el punto. Usamos inset-ring porque va por dentro y no cambia el tamaño de la insignia. La opacidad /30 hace que el anillo combine con el fondo claro.

Fíjate en que el estado no se comunica solo con el color: el texto sigue diciendo cuál es. Quien no distinga colores puede leerlo igualmente.

Ejercicio 2: Ficha de producto 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), drop-shadow-xl y backdrop-blur-sm dan otro valor en esta tarjeta. Si usas la LTS: escribe drop-shadow-[0_9px_7px_rgb(0_0_0/0.1)] y backdrop-blur para que se vea igual.

Crea una tarjeta de 288 px de ancho para el “Portátil UltraBook 14” (899,00 €). La cabecera tendrá /img/escena.svg de fondo, la imagen del portátil centrada con una sombra que siga su forma, y una insignia “Novedad” de cristal. Las esquinas de la cabecera deben seguir el radio de la tarjeta, y la sombra debe crecer al pasar el ratón.

Ver solución
<article class="w-72 overflow-hidden rounded-2xl bg-white shadow-md ring-1 ring-slate-900/5 transition-shadow hover:shadow-xl">
  <div class="relative h-40 bg-[url(/img/escena.svg)] bg-cover bg-center">
    <img src="/img/portatil.svg" alt="Portátil UltraBook 14" class="absolute inset-0 m-auto h-28 drop-shadow-xl">
    <span class="absolute top-3 left-3 rounded-full bg-black/40 px-2.5 py-0.5 text-xs font-medium text-white ring-1 ring-white/30 backdrop-blur-sm">
      Novedad
    </span>
  </div>
  <div class="p-4">
    <h3 class="font-semibold">Portátil UltraBook 14</h3>
    <p class="text-sm text-slate-500">Portátil ligero de 14 pulgadas, 16 GB RAM</p>
    <div class="mt-3 flex items-center justify-between">
      <span class="text-lg font-bold">899,00 €</span>
      <button type="button" class="rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-semibold text-white">Añadir</button>
    </div>
  </div>
</article>

Tres detalles marcan la diferencia. overflow-hidden en el <article> hace que la cabecera respete el radio sin necesidad de rounded-t-2xl. La sombra y el anillo están en el mismo <article>, de modo que overflow-hidden no los recorta: solo afecta a los hijos. Y drop-shadow-xl sigue la silueta del portátil porque el SVG es transparente.

La prueba automática pasó el ratón sobre la tarjeta y comprobó que el box-shadow calculado cambia. transition-shadow anima ese cambio.

Ejercicio 3: Pestañas que no saltan

Crea la navegación de las categorías Portátiles, Sobremesa y Periféricos, con Sobremesa como pestaña activa. La activa llevará una línea inferior de 2 px, que debe coincidir exactamente con la línea gris que recorre toda la navegación. Al activar una pestaña nada debe moverse.

Ver solución
<nav class="flex gap-6 border-b border-slate-200 text-sm font-medium" aria-label="Catálogo">
  <a href="#" class="-mb-px border-b-2 border-transparent px-1 py-3 text-slate-500 hover:border-slate-300 hover:text-slate-700">Portátiles</a>
  <a href="#" aria-current="page" class="-mb-px border-b-2 border-indigo-600 px-1 py-3 text-indigo-600">Sobremesa</a>
  <a href="#" class="-mb-px border-b-2 border-transparent px-1 py-3 text-slate-500 hover:border-slate-300 hover:text-slate-700">Periféricos</a>
</nav>
Medido en Chromium:
altura de cada enlace:     46, 46, 46 px     (activa e inactivas son iguales)
parte inferior del enlace: 78 px
parte inferior del nav:    78 px             (las dos líneas coinciden)

Hay dos técnicas. border-b-2 border-transparent en las inactivas reserva el sitio, y así todas miden lo mismo. -mb-px baja cada enlace 1 px para que su línea tape la gris de nav, en lugar de quedar encima de ella.

aria-current="page" indica la pestaña activa a los lectores de pantalla. Es lo que falta en muchas pestañas que solo se distinguen por el color.

Ejercicio 4: Plantilla filtrable sin línea sobrante

Crea una lista de empleados con separadores y un desplegable que filtre por departamento, con cuatro empleados de los departamentos 10, 20, 20 y 30. Prueba primero con divide-y y observa qué pasa al filtrar Ventas. Después corrige el defecto para que nunca quede una línea sobrante.

Ver solución
<div class="w-96">
  <label class="mb-3 block text-sm font-medium">Departamento
    <select id="filtro" class="ms-2 rounded-md border border-slate-300 px-2 py-1">
      <option value="">Todos</option>
      <option value="20">Ventas</option>
      <option value="10">Dirección</option>
    </select>
  </label>
  <ul id="plantilla" class="rounded-xl border border-slate-200 [&>:not([hidden])~:not([hidden])]:border-t [&>:not([hidden])~:not([hidden])]:border-slate-200">
    <li data-dept="10" class="p-3">Ana García López <span class="text-sm text-slate-500">Director general</span></li>
    <li data-dept="20" class="p-3">Luis Martínez Ruiz <span class="text-sm text-slate-500">Director</span></li>
    <li data-dept="20" class="p-3">Carmen Ruiz Ortega <span class="text-sm text-slate-500">Vendedora</span></li>
    <li data-dept="30" class="p-3">Marta Sánchez Gil <span class="text-sm text-slate-500">Directora</span></li>
  </ul>
</div>

<script>
  const filtro = document.querySelector('#filtro');
  filtro.addEventListener('change', () => {
    for (const empleado of document.querySelectorAll('#plantilla > li')) {
      empleado.hidden = filtro.value !== '' && empleado.dataset.dept !== filtro.value;
    }
  });
</script>
Bordes medidos tras cada filtro (t = arriba, b = abajo):
Todos      Ana t=0   Luis t=1   Carmen t=1   Marta t=1
Ventas     Luis t=0  Carmen t=1
Dirección  Ana t=0

Con divide-y en v4, al filtrar Ventas el <li> de Marta queda oculto, pero sigue siendo el :last-child. Por eso Carmen, que no es el último hijo, conserva su border-bottom. La línea toca el borde del contenedor y forma una raya doble.

La variante arbitraria aplica el borde superior a cada elemento visible que tenga otro visible antes. Es lo mismo que hacía el divide-y de v3: el primer visible nunca lleva línea. Por eso, como ves en la salida, cada filtro deja a su primer elemento con t=0.

Ejercicio 5: Botones accesibles 🆕 v4.0

🆕 Desde Tailwind 4.0. No existe en la LTS (Tailwind 3.4). Si usas la LTS: cambia inset-ring inset-ring-slate-300 por ring-1 ring-inset ring-slate-300 y añade focus-visible:outline a cada botón, porque outline-2 no se ve sola.

Crea cuatro botones: Guardar (principal), Cancelar (secundario, blanco con anillo interior gris), Eliminar (peligro) y uno desactivado. Los tres activos deben tener un indicador de foco con teclado que siga visible en colores forzados, sin moverse ni tapar a sus vecinos. Pruébalo con el tabulador.

Ver solución
<div class="flex flex-wrap gap-4">
  <button type="button" class="rounded-lg bg-indigo-600 px-4 py-2 font-semibold text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Guardar</button>
  <button type="button" class="rounded-lg bg-white px-4 py-2 font-semibold text-slate-800 inset-ring inset-ring-slate-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Cancelar</button>
  <button type="button" class="rounded-lg bg-rose-600 px-4 py-2 font-semibold text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rose-600">Eliminar</button>
  <button type="button" disabled class="rounded-lg bg-indigo-600 px-4 py-2 font-semibold text-white disabled:opacity-50">Sin permiso</button>
</div>
Pulsando el tabulador con Playwright:
Modo normal       Guardar, Cancelar, Eliminar: outline solid 2px, offset 2px
Colores forzados  Guardar, Cancelar, Eliminar: outline solid 2px, offset 2px
Tras Eliminar     el foco sale del grupo: el botón desactivado no lo recibe

outline resuelve todo a la vez: no ocupa sitio, tiene un hueco real y resiste los colores forzados. El gap-4 deja sitio de sobra para los 4 px que necesita (2 de contorno y 2 de hueco).

Ejercicio 6: Navegación de cristal

Crea una barra superior fija sobre una página cuyo fondo sea /img/escena.svg. Debe desenfocar lo que pasa por debajo, ser legible si el navegador no admite backdrop-filter, y quedarse opaca si el usuario ha pedido reducir las transparencias.

Ver solución
<body class="bg-[url(/img/escena.svg)] bg-cover bg-fixed text-slate-900">
  <header class="sticky top-0 flex items-center justify-between border-b border-white/40 bg-white/90 px-6 py-3 shadow-md backdrop-blur-md supports-[backdrop-filter]:bg-white/50 [@media(prefers-reduced-transparency:reduce)]:bg-white">
    <strong>Tienda EMPRESA</strong>
    <nav class="flex gap-5 text-sm">
      <a href="#">Portátiles</a><a href="#">Sobremesa</a><a href="#">Periféricos</a>
    </nav>
  </header>
  <main class="h-[150vh] p-6 text-white">
    <p>Desplázate y mira cómo se difumina lo que pasa por debajo de la barra.</p>
  </main>
</body>
Medido en Chromium:
Normal                 fondo blanco al 50 %, backdrop-filter: blur(12px)
Transparencia reducida fondo rgb(255, 255, 255) opaco

Hay tres niveles, de menos a más exigente. bg-white/90 es la base: si backdrop-filter no existe, el texto se lee igualmente. supports-[backdrop-filter]:bg-white/50 solo se aplica si el navegador sabe filtrar, y entonces puede ser más transparente. [@media(prefers-reduced-transparency:reduce)]:bg-white tiene la última palabra.

Este ejemplo compila igual en v3 y en v4, y lo hemos medido en las dos. Por eso usa shadow-md, que vale lo mismo en ambas, y no shadow-sm. supports-[...] y las variantes arbitrarias se explican en el módulo 11.

Ejercicio 7: Categorías en gris que recuperan el color

En la portada de la tienda queremos tres categorías con su imagen en blanco y negro y atenuada, que recuperen el color de forma suave al pasar el ratón.

Ver solución
<ul class="grid w-[560px] grid-cols-3 gap-6">
  <li><img src="/img/portatil.svg" alt="Portátiles" class="grayscale opacity-60 transition-[filter,opacity] duration-300 hover:grayscale-0 hover:opacity-100"></li>
  <li><img src="/img/torre.svg" alt="Sobremesa" class="mx-auto h-28 grayscale opacity-60 transition-[filter,opacity] duration-300 hover:grayscale-0 hover:opacity-100"></li>
  <li><img src="/img/teclado.svg" alt="Periféricos" class="grayscale opacity-60 transition-[filter,opacity] duration-300 hover:grayscale-0 hover:opacity-100"></li>
</ul>
Medido con Playwright sobre la primera imagen:
En reposo            filter: grayscale(1)   opacity: 0.6
Tras pasar el ratón  filter: grayscale(0)   opacity: 1

grayscale-0 no quita el filtro, sino que pone grayscale(0). Es deliberado: una transición necesita dos valores numéricos entre los que interpolar. Con filter-none el salto sería brusco.

transition-[filter,opacity] limita la animación a esas dos propiedades, en lugar de a todas. Las transiciones se tratan en el módulo 13.

Recuerda que un móvil no tiene ratón: ahí estas imágenes se quedarán en gris. Si el color es importante, cambia la lógica y aplica el gris solo en pantallas con hover.

Ejercicio 8: Cabecera fundida en v4 y en v3 🆕 v4.1

🆕 Desde Tailwind 4.1. No existe en la LTS (Tailwind 3.4). Si usas la LTS: haz directamente la segunda parte del ejercicio, con [mask-image:linear-gradient(...)].

Crea una cabecera de 288 px de alto con /img/escena.svg, que se desvanezca hacia abajo entre el 30 % y el 95 % de su altura y deje ver el título sobre el fondo de la página. Escríbela con mask-* y después tradúcela a una versión que compile en Tailwind 3.4. Comprueba que las dos dan la misma máscara.

Ver solución

Versión para v4.1 o superior:

<header class="relative h-72 overflow-hidden">
  <img src="/img/escena.svg" alt="" class="absolute inset-0 h-full w-full object-cover mask-b-from-30% mask-b-to-95%">
  <div class="absolute inset-x-0 bottom-4 text-center">
    <h1 class="text-3xl font-black">Rebajas de otoño</h1>
    <p class="text-slate-600">Hasta un 30 % en portátiles seleccionados</p>
  </div>
</header>

Versión para v3.4, que solo cambia la clase de la máscara:

<img src="/img/escena.svg" alt="" class="absolute inset-0 h-full w-full object-cover [mask-image:linear-gradient(to_bottom,black_30%,transparent_95%)]">
mask-image calculado en Chromium:
v4.3.3   linear-gradient(blanco), linear-gradient(blanco),
         linear-gradient(rgb(0, 0, 0) 30%, rgba(0, 0, 0, 0) 95%), linear-gradient(blanco)...
v3.4.19  linear-gradient(rgb(0, 0, 0) 30%, rgba(0, 0, 0, 0) 95%)

Las capturas de las dos versiones son iguales. La de v4 tiene más capas, porque cada lado que no usas ocupa su sitio con un degradado blanco, que al intersecarse no recorta nada. Ese es el precio de poder combinar mask-b-*, mask-r-* y demás con solo añadir clases. En v3, con un valor arbitrario, combinarlas te tocaría a ti.

Ejercicio 9 (propuesto): De v4 a v3

Toma el caso completo de la sección 21 y compílalo en Tailwind 3.4. Sustituye cada clase que no exista y revisa también las que existen pero cambian de valor, como shadow-sm, drop-shadow-xl o blur-sm. Haz una captura de cada versión y compáralas.

Ejercicio 10 (propuesto): Sistema de elevación 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4), el tema no se define con @theme. Si usas la LTS: define las tres sombras en theme.extend.boxShadow, dentro de tailwind.config.js.

Define en @theme tres sombras propias para tu tienda: --shadow-tarjeta, --shadow-menu y --shadow-modal. Todas deben tener el mismo color azulado y una altura creciente. Crea una página con los tres niveles sobre fondo claro y oscuro, y decide qué anillo añadir en oscuro para que se sigan distinguiendo.

Ejercicio 11 (propuesto): Grupo de avatares

Crea cinco avatares circulares de size-12, solapados con -space-x-2, cada uno con ring-2 ring-white para separarlos. Al pasar el ratón sobre uno, debe colocarse por encima de sus vecinos sin que nada se mueva. Prueba después con un fondo oscuro y explica por qué el anillo debe cambiar de color. ¿Podrías resolverlo con ring-offset-*?

Ejercicio 12 (propuesto): Vista previa de factura 🆕 v4.3

🆕 Desde Tailwind 4.3. No existe en la LTS (Tailwind 3.4). Si usas la LTS: usa [zoom:0.5] en lugar de zoom-50 y un valor [mask-image:...] para el sombreado.

Maqueta una factura de EMPRESA a tamaño completo y muéstrala dentro de un panel como vista previa con zoom-50. Mide el espacio que ocupa en la página y repítelo con scale-50. Explica la diferencia. Añade después un sombreado con mask-b-from-* para indicar que la factura continúa, y comprueba cómo queda al imprimir la página.


Resumen

Concepto API Idea clave
Bordes border, border-t-2, border-dashed, border-slate-200 Ocupan sitio; en v4 el color por defecto es currentColor
Radios rounded-lg, rounded-t-2xl, rounded-full No recortan a los hijos: overflow-hidden
Anillos ring-2, ring-inset, ring-offset-2 Sombra sólida; ring mide 1 px en v4 y 3 px en v3
Contornos outline-2, outline-offset-2 Hueco real y visibles en colores forzados
Foco focus-visible:outline-2, outline-hidden outline-none de v4 elimina el contorno del todo
Separadores divide-y, divide-slate-200 En v4, cuidado con el último hijo oculto
Sombras shadow-md, shadow-indigo-500/50 shadow-sm de v4 es shadow de v3
Opacidad de sombra shadow-lg/50 (4.1) Sustituye la opacidad, no la reduce
Capas de box-shadow inset-shadow-sm, inset-ring (4.0) Capas distintas se suman; la misma, se pisa
Opacidad opacity-50 frente a bg-white/50 La primera afecta a todo el elemento
Mezcla mix-blend-multiply, bg-blend-luminosity, isolate isolate limita con qué se mezcla
Filtros blur-sm, grayscale, brightness-75 Se combinan en un orden fijo
Sombra con forma drop-shadow-lg, drop-shadow-indigo-500 (4.1) Sigue la transparencia; md, lg y xl cambian de valor en v4
Cristal backdrop-blur-md con bg-white/30 Necesita fondo translúcido; cuidado con los antepasados
Efectos secundarios isolate, opacity, filter, máscaras Atrapan el z-index; filter también atrapa a fixed
Máscaras mask-b-from-50%, mask-radial-from-40% (4.1) Se intersecan; from y to son las paradas
Zoom zoom-125 (4.3) Recalcula el diseño, al contrario que scale-*
Renombrados -sm pasa a -xs; sin sufijo pasa a -sm -sm es el falso amigo
Valores propios rounded-(--radio), --radius-*, --shadow-* En v3, corchetes con pista de tipo y tailwind.config.js