Módulo 07: Layout y posicionamiento

Objetivos del módulo

  • Elegir el display adecuado y saber qué cambia entre block, inline, inline-block, flow-root y contents
  • Distinguir las cuatro formas de ocultar un elemento y qué ocurre con el espacio, el foco y los lectores de pantalla
  • Predecir qué clase gana cuando se juntan hidden y otra de display, porque el resultado depende de la versión
  • Colocar elementos con relative, absolute, fixed y sticky, sabiendo siempre respecto a qué se colocan
  • Diagnosticar por qué un fixed no se queda fijo o un sticky no se pega
  • Dominar z-* entendiendo los contextos de apilamiento, y usar isolate para que no se mezclen
  • Controlar el desbordamiento con overflow-* y la cadena de desplazamiento con overscroll-*
  • Usar float-*, columns-*, aspect-* y object-* en los casos en que siguen siendo la mejor herramienta
  • Configurar container en v4 con @utility y en v3 con tailwind.config.js
  • Ajustar el desplazamiento con scroll-* y crear carruseles con snap-* sin JavaScript
  • Dar estilo a las barras de desplazamiento con scrollbar-*, y conseguir lo mismo en la LTS
  • Montar la estructura completa de una página de la tienda con todas estas piezas

1. Preparación del laboratorio

Sigues con el proyecto Vite del módulo 01: un index.html y un app.css con @import "tailwindcss";. Crea una página .html en la raíz para cada apartado y guarda app.css si una clase nueva no aparece.

Varios ejemplos usan una foto. Descarga esta imagen de un portátil y guárdala como foto.svg en la carpeta public del proyecto, o en la raíz si no tienes esa carpeta. Tiene cuatro esquinas marcadas con letras (A, B, C y D) y una E en el centro: así verás de un vistazo qué parte de la foto se recorta en cada caso.

Este módulo reparte el espacio en la página, pero la colocación entre hermanos con Flexbox y Grid llega en los módulos siguientes. Para que las figuras sean legibles usaremos algunas clases que aún no has estudiado:

Clases Qué hacen Dónde se estudian
flex, gap-4, items-center, justify-between, shrink-0, self-start Fila flexible y su alineación Flexbox
grid, grid-cols-3, grid-cols-[12rem_1fr] Rejilla CSS Grid
sm:, md:, lg: Aplican una clase a partir de un ancho Diseño responsive
dark:, focus:, motion-safe: Aplican una clase en una condición Modo oscuro y Variantes y estados
-translate-x-1/2, rotate-45 Mueven o giran sin alterar el flujo Transiciones, animaciones y 3D

Si necesitas repasar el modelo de caja, position o z-index en CSS puro, tienes el Curso de HTML y CSS.


2. display: cómo participa una caja en el flujo

El flujo normal es la forma en que el navegador coloca las cajas si no le dices nada: los bloques se apilan de arriba abajo y los elementos en línea se colocan seguidos, como las palabras de un texto. La propiedad display decide cómo entra cada caja en ese reparto. Casi todas las clases son traducciones directas:

Clase CSS generado Comportamiento
block display: block Ocupa toda la línea y admite ancho, alto y márgenes verticales
inline display: inline Fluye con el texto. Ignora w-* y h-*
inline-block display: inline-block Fluye con el texto, pero admite ancho y alto
flex, inline-flex display: flex, inline-flex Contenedor flexible (módulo 08)
grid, inline-grid display: grid, inline-grid Contenedor de rejilla (módulo 09)
flow-root display: flow-root Bloque que crea su propio contexto y contiene sus floats
contents display: contents La caja desaparece y sus hijas suben un nivel
table, table-row, table-cell… display: table… Se comporta como una tabla sin serlo
list-item display: list-item Bloque con marcador de lista
hidden display: none No se genera ninguna caja

block, inline e inline-block

La diferencia se ve mejor con la misma frase y el mismo w-40 h-12 en los tres casos:

<p>
  Stock de
  <span class="inline-block w-40 h-12 bg-indigo-200">Portátil UltraBook 14</span>
  disponible.
</p>

Tres párrafos con los mismos elementos: en inline el ancho se ignora, en inline-block se aplica sin romper la línea y en block cada elemento va en su línea

Figura 1. Un span con w-40 h-12 en los tres modos. Solo inline ignora el tamaño.

Dos consecuencias prácticas:

  • Por eso un <a> no acepta bien el relleno vertical. Un enlace es inline: su py-3 se pinta, pero no empuja a las líneas de alrededor. Para que un enlace se comporte como botón, añade inline-block o inline-flex.
  • Preflight convierte las imágenes en bloques. Como viste en el módulo 02, img, svg y video llevan display: block. Esto evita el hueco que aparece bajo una imagen en línea, pero significa que una imagen dentro de un texto saltará a su propia línea. Si quieres un icono en medio de una frase, añade inline.

flow-root y contents

Estas dos clases son las menos conocidas, y ambas resuelven problemas concretos.

flow-root genera un bloque normal con una diferencia: lo que pasa dentro se queda dentro. Sus floats no se salen y los márgenes de sus hijas no atraviesan su borde. Lo usarás en la sección 6.

contents hace lo contrario: elimina la caja del elemento y deja a sus hijas como si fueran hijas directas del abuelo. Es útil cuando un envoltorio estorba en una rejilla:

<div class="grid grid-cols-3">
  <a href="/ofertas" class="contents">
    <span>Uno</span>
    <span>Dos</span>
    <span>Tres</span>
  </a>
</div>

Sin contents, el <a> sería la única celda de la rejilla y los tres span se amontonarían en la primera columna. Con contents, en la prueba cada span empezó en una columna distinta (x = 0, 328 y 657 px). A cambio, el <a> ya no tiene caja, así que su relleno, su fondo y su borde no se pintan.

Quién gana entre dos clases de display 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) hidden gana siempre a cualquier otra clase de display. En 4.0 solo gana a block, contents, flex, flow-root y grid, y pierde frente a inline, inline-block, inline-flex, inline-grid, list-item y las de tabla. Si usas la LTS: tu código funciona, pero si migras revisa cada classList.add('hidden'). Lo más seguro en ambas versiones es no juntar nunca dos clases de display.

Como viste en el módulo 02, con dos clases que tocan la misma propiedad no gana la última del atributo class: gana la que esté más abajo en la hoja. En v3 el orden es el que escribieron los autores, con hidden al final. En v4 las utilidades de una misma propiedad se ordenan alfabéticamente:

v3: block inline-block inline flex inline-flex table table-cell table-row
    flow-root grid inline-grid contents list-item hidden

v4: block contents flex flow-root grid hidden inline inline-block inline-flex
    inline-grid list-item table table-cell table-row

Lo comprobé midiendo el display real en Chromium con la hoja de cada versión:

Clases del elemento v3.4.19 v4.3.3
flex hidden none none
block hidden none none
grid hidden none none
inline hidden none inline
inline-block hidden none inline-block
inline-flex hidden none inline-flex
table hidden none table
list-item hidden none list-item

La guía oficial de actualización no recoge este cambio de orden, y es fácil tropezar con él. Un botón inline-flex al que un script le añade hidden deja de ocultarse en v4, sin ningún error en la consola:

<a id="carrito" class="inline-flex items-center gap-2">Ver cesta</a>

<script>
  // En v3 se oculta. En v4 sigue visible.
  document.querySelector('#carrito').classList.add('hidden');
</script>

Lo correcto en responsive sí funciona en ambas versiones, porque las variantes van siempre después de las utilidades sin variante:

<!-- Oculto en móvil, visible desde md -->
<nav class="hidden md:inline-flex">...</nav>

Formas de ocultar un elemento

Ocultar no es una sola cosa. Hay que decidir qué pasa con el hueco, con el teclado y con los lectores de pantalla.

Cuatro filas con tres cajas. Con hidden, Tres se mueve al sitio de Dos. Con invisible y opacity-0 se mantiene el hueco

Figura 2. hidden retira la caja. invisible y opacity-0 conservan su hueco.

Clase ¿Ocupa sitio? ¿Recibe el foco? ¿Lo lee un lector de pantalla? Cuándo usarla
hidden No No No Quitar algo por completo
invisible Sí No No Conservar el hueco y evitar saltos
opacity-0 Sí Sí Sí Base de una animación de aparición
sr-only Casi nada (1 px) Sí Sí Texto solo para lectores de pantalla

La columna de foco es la que más sorprende. Medí con element.focus() en cada caso: hidden e invisible no reciben el foco, mientras que opacity-0 y sr-only sí. Un botón con opacity-0 sigue en el orden de tabulación y sigue recibiendo clics, de modo que puedes pulsar algo que no ves.

invisible tiene una rareza que no comparte con hidden: la visibilidad se hereda, pero una hija puede rectificarla.

<div class="invisible">
  <button class="visible">Sigo visible aunque mi padre no lo esté</button>
</div>

Con hidden esto es imposible: si el padre no tiene caja, las hijas tampoco. En la prueba, el botón dentro de un padre hidden midió 0 × 0 px.

La tercera utilidad de visibility es collapse, pensada para filas de tabla. Retira la fila sin recalcular el ancho de las columnas. En la prueba, una tabla cuya fila larga llevaba collapse mantuvo sus 193,5 px de ancho, y con hidden la columna se encogió a 37,1 px. Sirve para mostrar y ocultar filas sin que la tabla baile.

El enlace de salto con sr-only

sr-only hace una caja de 1 px recortada, y not-sr-only deshace todo eso. Juntas sirven para los enlaces de salto, que permiten a quien navega con teclado evitar el menú:

<a href="#catalogo"
   class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:rounded focus:bg-white focus:px-4 focus:py-2">
  Saltar al catálogo
</a>

Pulsa Tab nada más cargar la página y aparece:

Cabecera de la tienda con un recuadro blanco arriba a la izquierda que dice Saltar al catálogo

Figura 3. El enlace de salto solo se ve cuando recibe el foco con el teclado.

El atributo hidden de HTML 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) Preflight da display: none al atributo hidden sin !important, así que flex o block lo anulan y el elemento se ve. En 4.0 la regla lleva !important y el atributo gana siempre. Si usas la LTS: añade [&[hidden]]:hidden a los elementos con clase de display, o alterna la clase hidden en lugar del atributo.

HTML tiene un atributo hidden y JavaScript su propiedad elemento.hidden = true. Preflight tiene una regla para él, y la diferencia entre versiones es esta palabra:

/* v3.4.19 */
[hidden]:where(:not([hidden="until-found"])) { display: none; }

/* v4.3.3 */
[hidden]:where(:not([hidden='until-found'])) { display: none !important; }

Unida al cambio de orden anterior, da una tabla curiosa. Esto es lo que medí con un elemento inline-flex:

Forma de ocultar v3.4.19 v4.3.3
classList.add('hidden') Se oculta No se oculta
elemento.hidden = true No se oculta Se oculta

Es decir, lo que funcionaba en una versión falla en la otra. La salida para escribir código válido en ambas es [&[hidden]]:hidden, que comprobé en las dos: da none con el atributo puesto incluso sobre un elemento flex.

<div hidden class="flex [&[hidden]]:hidden">Panel</div>

3. Posicionamiento

position decide dos cosas: si la caja conserva su sitio en el flujo y respecto a qué se miden top, right, bottom y left.

Clase CSS ¿Sigue en el flujo? Referencia de top, left…
static position: static Sí Ninguna: esas propiedades se ignoran
relative position: relative Sí, deja su hueco Su propia posición normal
absolute position: absolute No El ancestro posicionado más cercano
fixed position: fixed No La ventana
sticky position: sticky Sí Su contenedor de desplazamiento

Cuatro paneles: relative se aleja de su hueco, absolute se coloca dentro de su padre relative, fixed en la ventana y sticky pegada arriba de su contenedor

Figura 4. Cada posición tiene su propia referencia. Casi todos los errores vienen de confundirla.

Un ancestro posicionado es cualquiera cuyo position no sea static. La figura siguiente es el resultado real de las cuatro situaciones básicas:

<!-- Patrón básico: el padre es la referencia y la hija se coloca dentro -->
<div class="relative h-32 border">
  <p>Texto normal</p>
  <div class="absolute top-2 right-2">Caja</div>
</div>

Cuatro recuadros. El último deja escapar una caja naranja que acaba en la esquina superior derecha de la página

Figura 5. Con el padre static, la caja naranja se escapa hasta la esquina de la página y tapa parte de la figura.

Lo que hay que grabarse: absolute casi siempre va acompañado de relative en el padre. Si la hija aparece en un sitio absurdo, lo primero que hay que mirar es si te falta ese relative.

inset-*, top-*, right-*, bottom-*, left-*

Estas clases usan la escala de espaciado que viste en el módulo 03, y además admiten fracciones, auto, full y px:

Clase CSS generado (v4) Uso
inset-0 inset: 0px Cubrir todo el padre
inset-x-0 inset-inline: 0px De lado a lado
inset-y-0 inset-block: 0px De arriba abajo
top-0 top: 0px Pegar arriba
right-4 right: calc(var(--spacing) * 4) A 1 rem del borde derecho
-top-2 top: calc(var(--spacing) * -2) Sobresale 0,5 rem por arriba
left-1/2 left: calc(1 / 2 * 100%) En la mitad del padre
inset-full inset: 100% Justo fuera del padre
inset-auto inset: auto Deshacer un inset-* anterior

Los porcentajes se calculan sobre el tamaño del padre: left-1/2 coloca el borde izquierdo de la caja en la mitad del padre, no el centro de la caja. Por eso centrar requiere un paso más.

Valores sin escala fija 🆕 v4.0

🆕 Desde Tailwind 4.0. No existe en la LTS (Tailwind 3.4), que solo acepta los valores de su escala. Si usas la LTS: para top-17 escribe top-[4.25rem], y para una variable, top-[--altura] en lugar de top-(--altura).

Como en el resto de la escala de espaciado, en v4 cualquier múltiplo vale: top-17 genera top: calc(var(--spacing) * 17). Es útil para colocar algo justo debajo de una cabecera de altura conocida. Los paréntesis usan una variable CSS: top-(--altura-cabecera) genera top: var(--altura-cabecera).

Dos formas de centrar

<!-- 1. Tamaño desconocido: se usa la propia medida de la caja -->
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">Agotado</div>

<!-- 2. Tamaño conocido: los márgenes automáticos reparten el sitio -->
<div class="absolute inset-0 m-auto size-20">Agotado</div>

En un padre de 384 × 384 px, las dos dejaron una caja de 80 px en la misma posición exacta. La primera funciona porque los porcentajes de translate se refieren a la propia caja: left-1/2 lleva el borde al centro y -translate-x-1/2 retrocede media caja. La segunda solo vale con un tamaño fijo, porque sin él inset-0 estiraría la caja.

Caso real: insignias sobre una tarjeta

Con esto ya puedes hacer las tres variantes habituales de una tarjeta de producto:

<!-- 1. Insignia dentro -->
<article class="relative overflow-hidden rounded-xl bg-white shadow-md">
  <img src="/foto.svg" alt="Portátil UltraBook 14" class="aspect-3/2 w-full object-cover">
  <span class="absolute top-3 left-3 rounded-full bg-indigo-600 px-3 py-1 text-xs font-bold text-white">
    Novedad
  </span>
  <div class="p-4">
    <h2 class="font-bold">Portátil UltraBook 14</h2>
    <p class="text-lg font-bold text-indigo-700">899 €</p>
  </div>
</article>

<!-- 2. Insignia que sobresale: la tarjeta no puede llevar overflow-hidden -->
<article class="relative rounded-xl bg-white shadow-md">
  <img src="/foto.svg" alt="Portátil Gaming 16" class="aspect-3/2 w-full rounded-t-xl object-cover">
  <span class="absolute -top-3 -right-3 flex size-12 items-center justify-center rounded-full bg-red-600 text-sm font-bold text-white shadow">
    -15%
  </span>
  <div class="p-4">
    <h2 class="font-bold">Portátil Gaming 16</h2>
    <p class="text-lg font-bold text-indigo-700">1274 €</p>
  </div>
</article>

<!-- 3. Capa que cubre la tarjeta y mensaje centrado -->
<article class="relative overflow-hidden rounded-xl bg-white shadow-md">
  <img src="/foto.svg" alt="Torre Oficina i5" class="aspect-3/2 w-full object-cover">
  <div class="p-4">
    <h2 class="font-bold">Torre Oficina i5</h2>
    <p class="text-lg font-bold text-indigo-700">649 €</p>
  </div>
  <div class="absolute inset-0 bg-white/70"></div>
  <span class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded bg-gray-900 px-4 py-2 font-bold text-white">
    Agotado
  </span>
</article>

Tres tarjetas de producto: una con la insignia Novedad, otra con un círculo de -15% que sobresale de la esquina y otra blanqueada con el rótulo Agotado

Figura 6. Los tres patrones: insignia interior, insignia que sobresale y capa completa.

Fíjate en el detalle de la segunda tarjeta: como la insignia sobresale, la tarjeta no puede llevar overflow-hidden. Por eso el redondeo superior se traslada a la imagen con rounded-t-xl.

💡 Consejo: Un <span> con absolute no necesita block: el navegador lo convierte en bloque al sacarlo del flujo. Por eso sus px-3 py-1 funcionan sin añadir nada.

inset-x-* e inset-y-* son lógicas 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) inset-x-0 genera left y right; en 4.0 genera inset-inline. Lo mismo ocurre con inset-y-*, scroll-mx-* y scroll-px-*. Si usas la LTS: en texto horizontal, sea de izquierda a derecha o al revés, el resultado es idéntico. Solo en writing-mode vertical necesitas escribir [inset-inline:0].

Las propiedades lógicas dependen del sentido de escritura, como viste con px-* en el módulo 03. Solo se nota en texto vertical. Dentro de un padre de 160 × 160 px con writing-mode: vertical-rl, inset-x-0 midió 160 de ancho y unos 7 de alto en v3, y 24 de ancho y 160 de alto en v4: en vertical, inline es el eje de arriba abajo.

inset-s-*, inset-e-*, inset-bs-* e inset-be-* 🆕 v4.2

🆕 Desde Tailwind 4.2. No existe en la LTS (Tailwind 3.4). Si usas la LTS: usa start-* y end-*, que generan lo mismo que inset-s-* e inset-e-*. Para el eje de bloque no hay utilidad: escribe [inset-block-start:1rem]. Tampoco hay scroll-pbs-* ni scroll-mbs-*.

Hasta ahora, para colocar algo “al principio de la línea”, que es la derecha en árabe o hebreo, existían start-* y end-*. La 4.2 las declara obsoletas en favor de nombres que dejan claro el eje:

Clase CSS generado Equivalente en 3.4
inset-s-4 inset-inline-start: calc(var(--spacing) * 4) start-4
inset-e-4 inset-inline-end: calc(var(--spacing) * 4) end-4
inset-bs-4 inset-block-start: calc(var(--spacing) * 4) No existe
inset-be-4 inset-block-end: calc(var(--spacing) * 4) No existe

start-* y end-* siguen generándose en 4.3.3, así que nada se rompe. Para código nuevo en v4 usa inset-s-*. Si mezclas físicas y lógicas, recuerda lo que ocurría con ml-* y ms-*: en left-6 start-2, el left calculado fue de 8 px en v3 (gana start) y de 24 px en v4 (gana left). No las combines.

fixed, y los ancestros que lo atrapan

fixed se coloca respecto a la ventana y no se mueve al desplazar la página. Es el que usarás para ventanas modales, avisos y botones flotantes:

<div class="fixed inset-x-0 bottom-0 z-40 bg-gray-900 p-3 text-white">
  Usamos cookies para recordar tu cesta.
</div>

Pero la referencia no siempre es la ventana. Un ancestro con transformación, filtro o algunas propiedades parecidas se convierte en la referencia de sus descendientes fixed. El elemento se queda encerrado y se desplaza con él. Probé con una hija fixed top-0 left-0 en cada caso, en las dos versiones:

Clase en el ancestro ¿Atrapa al fixed?
-translate-x-1/2 Sí
translate-x-0 (aunque no mueve nada) Sí
scale-100 (aunque no escala nada) Sí
blur Sí
backdrop-blur Sí
opacity-50 No
shadow-md No

La fila que más daño hace es backdrop-blur. Una cabecera translúcida con backdrop-blur es hoy muy común; si dentro de ella pones el menú móvil con fixed inset-0, el menú medirá lo que mide la cabecera y no la pantalla. La solución es colocar los paneles fixed fuera de ese ancestro, como hermanos de la cabecera. Es lo que hace el caso completo de la sección 14.

sticky, el más malentendido

sticky se comporta como relative hasta que la caja llega a un umbral, y entonces se queda pegada mientras su padre siga en pantalla.

Tres fases: la caja sube con la página, se queda pegada arriba mientras el padre sigue subiendo y finalmente se va con el final del padre

Figura 7. sticky nunca sale de su padre. Cuando el padre se acaba, la caja se va con él.

La clase se usa junto a un umbral: sticky top-0. Pero sticky falla en silencio. En la figura, las cuatro listas tienen el mismo contenido y se han desplazado lo mismo, y solo dos conservan la cabecera:

<div class="h-64 overflow-y-auto">
  <div class="overflow-clip">           <!-- con overflow-hidden no se pegaría -->
    <div class="sticky top-0 bg-indigo-600 p-2 text-white">Portátiles</div>
    <div class="p-2">...lista larga...</div>
  </div>
</div>

Cuatro listas desplazadas. En la primera y la cuarta la cabecera sigue arriba; en la segunda y la tercera ha desaparecido

Figura 8. Tras desplazar 90 px, la cabecera sigue arriba con top-0 y con overflow-clip, y se va sin top-* y con un padre overflow-hidden.

Las causas casi siempre son estas cuatro:

Fallo Por qué Qué hacer
Falta top-* Sin umbral, sticky es como relative Añade top-0 o el valor que toque
Un ancestro tiene overflow-hidden, -auto o -scroll Ese ancestro pasa a ser su contenedor de desplazamiento, y si él no se desplaza no hay nada a lo que pegarse Quita el overflow o cámbialo por overflow-clip
El padre mide lo mismo que la caja No le queda recorrido: sticky no sale de su padre Sube la clase al elemento que tenga altura, o saca la caja del envoltorio
La caja se estira en una rejilla o una fila flexible Ocupa toda la altura de la fila y tampoco tiene recorrido Añade self-start

La última es la más traicionera. En una maqueta con barra lateral, una celda de rejilla se estira por defecto hasta la altura de la fila. El aside medía 1500 px, igual que el contenido, y al desplazar se fue con él (y = −428 px). Con self-start midió 108 px y se quedó fijo a 16 px del borde superior.

Tabla con cabecera y primera columna fijas

sticky se puede usar en dos ejes a la vez, en cualquier contenedor con desplazamiento. Es la forma más sencilla de hacer una tabla grande manejable:

<div class="h-52 overflow-auto rounded-lg border border-gray-300">
  <!-- min-w-3xl es de v4; en v3, min-w-[48rem] -->
  <table class="w-full min-w-3xl text-left text-sm whitespace-nowrap">
    <thead>
      <tr>
        <th class="sticky top-0 left-0 z-20 bg-indigo-700 p-3 text-white">Producto</th>
        <th class="sticky top-0 z-10 bg-indigo-600 p-3 text-white">Descripción</th>
        <th class="sticky top-0 z-10 bg-indigo-600 p-3 text-white">Precio</th>
        <th class="sticky top-0 z-10 bg-indigo-600 p-3 text-white">Stock</th>
        <th class="sticky top-0 z-10 bg-indigo-600 p-3 text-white">Alta</th>
      </tr>
    </thead>
    <tbody class="divide-y divide-gray-200">
      <tr>
        <th class="sticky left-0 bg-white p-3">Portátil UltraBook 14</th>
        <td class="p-3">Portátil ligero de 14 pulgadas, 16 GB RAM</td>
        <td class="p-3">899 €</td>
        <td class="p-3">25</td>
        <td class="p-3">2024-01-10</td>
      </tr>
      <!-- ...más filas... -->
    </tbody>
  </table>
</div>

Tabla desplazada en los dos ejes: la fila de encabezados y la columna Producto siguen en su sitio

Figura 9. La tabla desplazada a la vez hacia abajo y hacia la derecha.

Tres detalles sin los que no queda bien:

  • Fondo opaco en las celdas pegadas. Sin bg-*, el contenido que pasa por debajo se transparenta.
  • z-* escalonado. La esquina (z-20) debe quedar por encima de la fila (z-10), que a su vez queda por encima del resto.
  • El sticky va en las celdas, no en <tr> ni en <thead>.

4. z-* y contextos de apilamiento

Cuando dos cajas se solapan, una tapa a la otra. Por defecto se pintan en el orden del HTML, con las posicionadas encima de las que no lo están. z-index cambia ese orden.

Clase CSS
z-0, z-10, z-20, z-30, z-40, z-50 z-index: 0, 10, …
z-auto z-index: auto
-z-10 z-index: calc(10 * -1)
z-[100] z-index: 100
isolate isolation: isolate
isolation-auto isolation: auto

z-index solo funciona en elementos posicionados y en hijas directas de flex o grid. En un div normal no hace nada. En la prueba, un z-20 sin posición dentro de una fila flex quedó por encima de un relative z-10, y al quitárselo quedó por debajo.

Valores dinámicos en z-* 🆕 v4.0

🆕 Desde Tailwind 4.0. No existe en la LTS (Tailwind 3.4), cuya escala llega a z-50. Si usas la LTS: escribe el valor arbitrario z-[100], que funciona en las dos versiones.

En v4, z-100 genera z-index: 100 sin configurar nada, igual que cualquier entero.

El contexto de apilamiento

Este es el concepto que resuelve casi todos los problemas de z-index. Un contexto de apilamiento es un grupo cerrado de capas. Los z-index del interior solo se comparan entre sí, y el grupo entero se pinta como una sola capa dentro de su propio padre.

Es como numerar páginas dentro de capítulos: la página 50 del capítulo 1 va antes que la página 10 del capítulo 2, por grande que sea el 50.

Árbol con la raíz y dos hijos. Cabecera con z-0 contiene un menú z-50. Contenido tiene z-10 y queda por encima

Figura 10. El menú compite dentro de la cabecera. Frente al exterior, la cabecera vale 0.

La situación más típica es un menú desplegable tapado por el contenido, por mucho z-* que le pongas:

<!-- El menú queda DEBAJO del contenido -->
<header class="relative z-0">
  Cabecera
  <div class="absolute top-8 left-2 z-50">Menú</div>
</header>
<main class="relative z-10">Contenido</main>

Seis paneles. En el segundo el menú z-50 queda cortado por el contenido z-10; en el tercero queda encima. Abajo, una esfera con -z-10 desaparece sin isolate y se ve con isolate

Figura 11. Arriba, el menú atrapado y el mismo menú liberado. Abajo, -z-10 con y sin isolate, y un z-50 sin efecto.

Hay dos arreglos, y los dos funcionan: quitar el z-0 de la cabecera, porque un relative sin z-* no crea contexto; o subir toda la cabecera con z-20. Lo que no sirve es cambiar z-50 por z-9999.

Qué crea un contexto de apilamiento

Lo difícil es que muchas clases que no parecen tener relación con el apilamiento crean uno sin avisar:

Crea contexto Ejemplo
Posicionado con z-index distinto de auto relative z-0
fixed y sticky, siempre sticky top-0
Hija de flex o grid con z-index z-10 en un hijo de flex
Opacidad menor que 1 opacity-90
Transformaciones scale-105, -translate-x-1/2
Filtros blur, backdrop-blur
Aislamiento isolate
Mezcla mix-blend-multiply

Por ejemplo, en la prueba una tarjeta con opacity-90 atrapó a su desplegable z-50, que quedó bajo el relative z-10 siguiente. Si un hover:opacity-90 hace que un menú desaparezca al pasar el ratón, ya sabes por qué.

isolate: crear un contexto a propósito

isolate crea un contexto sin más efectos. Tiene dos usos muy buenos.

Encerrar los z-* de un componente. Si cada tarjeta lleva isolate, sus z-10 internos no pueden competir con la cabecera de la página.

Hacer utilizable -z-10. Un -z-10 manda la caja detrás de todo lo del contexto actual, incluido el fondo de sus ancestros, y por eso parece que desaparece (figura 11, abajo a la izquierda). Con isolate en la sección, el fondo de esta se convierte en lo más profundo de su propio contexto, y la capa se queda justo encima de él. Es la forma limpia de poner una imagen de fondo real:

<section class="relative isolate overflow-hidden">
  <img src="/foto.svg" alt="" class="absolute inset-0 -z-10 size-full object-cover">
  <div class="absolute inset-0 -z-10 bg-indigo-950/60"></div>
  <h1 class="py-16 text-white">Rebajas de otoño</h1>
</section>

Una escala propia de capas 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) la escala se amplía en tailwind.config.js. Si usas la LTS: escribe theme: { extend: { zIndex: { modal: '100' } } } y tendrás la misma clase z-modal.

En un proyecto grande conviene dar nombre a las capas, en vez de ir improvisando números:

@import "tailwindcss";

@theme {
  --z-index-cabecera: 30;
  --z-index-panel: 40;
  --z-index-modal: 50;
}

Esto genera z-cabecera, z-panel y z-modal. El espacio de nombres es --z-index-*; con --z-modal, que parece más natural, comprobé que no se genera ninguna clase. Lo verás a fondo en el módulo 14.


5. overflow-* y overscroll-*

overflow decide qué hace una caja cuando su contenido no cabe.

Clase CSS ¿Recorta? ¿Se desplaza? ¿Crea contenedor de desplazamiento?
overflow-visible overflow: visible No No No
overflow-hidden overflow: hidden Sí Solo por código o con el foco Sí
overflow-clip overflow: clip Sí No No
overflow-auto overflow: auto Sí Sí, barras si hacen falta Sí
overflow-scroll overflow: scroll Sí Sí, barras siempre Sí

También existen por eje: overflow-x-auto, overflow-y-scroll, overflow-x-clip…

Cinco cajas con el mismo texto: en visible se sale, en hidden y clip se corta, en auto hay barra y en scroll aparecen las barras aunque el texto es corto

Figura 12. Los cinco modos. overflow-scroll muestra las barras aunque no hagan falta.

La regla práctica: usa overflow-auto para desplazarte y overflow-clip para recortar, y deja overflow-hidden para cuando necesites esa pequeña diferencia, porque overflow-clip no crea contenedor de desplazamiento y, por tanto, no rompe los sticky de dentro (figura 8).

La trampa de combinar ejes

Es natural querer overflow-x-auto y que un menú salga por abajo. CSS no lo permite: si un eje no es visible ni clip, el otro visible se convierte en auto. Lo medí:

Clases overflow-x real overflow-y real
overflow-x-auto overflow-y-visible auto auto
overflow-x-hidden overflow-y-visible hidden auto
overflow-x-clip overflow-y-visible clip visible

Por eso un desplegable dentro de una tabla con overflow-x-auto queda cortado, y por eso overflow-x-clip es la solución si solo quieres cortar en horizontal, por ejemplo una animación que entra desde el lateral y no debe generar barra.

¿Recorta overflow-hidden a un hijo absolute?

Depende de quién sea su referencia. El recorte afecta a los descendientes cuya caja de referencia está dentro del elemento con overflow:

<!-- Recortada: el padre con overflow es su referencia -->
<div class="relative h-10 overflow-hidden">
  <div class="absolute top-20">No se ve</div>
</div>

<!-- Se escapa: su referencia está más arriba y overflow no la afecta -->
<div class="h-10 overflow-hidden">
  <div class="absolute top-20">Se ve</div>
</div>

Comprobado en ambas versiones. Quitar el relative es un truco legítimo cuando un menú debe salir de una tarjeta con overflow-hidden, aunque el menú se colocará entonces respecto a otro elemento.

Bloquear el salto de la cadena: overscroll-*

Cuando llegas al final de una lista con desplazamiento interno y sigues girando la rueda, por defecto el movimiento pasa a la página. En un panel lateral o una ventana modal es muy molesto.

Clase CSS
overscroll-auto overscroll-behavior: auto
overscroll-contain overscroll-behavior: contain
overscroll-none overscroll-behavior: none
overscroll-y-contain, overscroll-x-none… Solo en un eje
<aside class="fixed inset-y-0 right-0 w-80 overflow-y-auto overscroll-contain">...</aside>

Lo probé girando la rueda 200 px sobre una lista ya en su final: sin overscroll-contain, la página bajó de 790 a 990 px; con él, se quedó en su sitio.


6. float y clear

Antes de Flexbox, float se usaba para maquetar columnas. Hoy no debes usarlo para eso, pero sigue siendo la única forma de que el texto rodee una imagen.

Clase CSS
float-left, float-right float: left, right
float-start, float-end float: inline-start, inline-end
float-none float: none
clear-left, clear-right, clear-both clear: left…
clear-start, clear-end, clear-none clear: inline-start…
<div class="rounded border p-3 text-sm">
  <img src="/foto.svg" alt="" class="float-left mr-3 mb-1 w-24 rounded">
  El Portátil UltraBook 14 pesa poco más de un kilo y aguanta toda la jornada...
</div>

A la izquierda, texto rodeando una imagen. En el centro la imagen sobresale de su padre. A la derecha el padre con flow-root la contiene

Figura 13. El texto rodea la imagen, y flow-root evita que el padre se colapse.

Un elemento flotante sale del flujo, así que su padre no lo cuenta al calcular su altura. Si el texto es más corto que la imagen, esta sobresale del padre (centro de la figura). La solución clásica era un clearfix. Hoy basta con flow-root en el padre:

<div class="flow-root rounded border p-3">
  <img src="/foto.svg" alt="" class="float-left mr-3 w-32 rounded">
  Poco texto.
</div>

clear-both hace lo contrario desde el otro lado: ese elemento baja hasta quedar debajo de cualquier float anterior.

⚠️ Importante: float-* no hace nada en una hija directa de flex o grid. En la prueba, un float-right dentro de una fila flex se quedó a la izquierda, aunque getComputedStyle siga diciendo right. Para mover una hija de flex usa ml-auto.


7. columns-*: varias columnas

columns reparte el contenido en columnas como las de un periódico. Es muy distinto de una rejilla: el contenido fluye de una columna a la siguiente, y no hay filas.

Clase CSS generado (v4) Significado
columns-2, columns-3… columns: 2 Número fijo de columnas
columns-3xs columns: var(--container-3xs) Ancho mínimo de 16 rem; caben las que quepan
columns-xs columns: var(--container-xs) Ancho de 20 rem
columns-7xl columns: var(--container-7xl) Ancho de 80 rem
columns-auto columns: auto Sin columnas
columns-[14rem] columns: 14rem Valor arbitrario

La versión por ancho es la más interesante, porque es adaptable sin puntos de ruptura: el navegador pone tantas columnas de al menos ese ancho como quepan. La separación se da con gap-*.

<div class="columns-3xs gap-6 text-justify text-sm">
  <h2 class="mb-2 font-bold">Condiciones de envío</h2>
  <p class="mb-2">Los pedidos realizados antes de las 16:00...</p>
  <p class="mb-2">Si no estás en casa, el repartidor deja un aviso...</p>
</div>

Número de columnas sin escala 🆕 v4.0

🆕 Desde Tailwind 4.0. No existe en la LTS (Tailwind 3.4), que tiene una escala fija. Si usas la LTS: para columns-16 escribe columns-[16].

En v4 vale cualquier entero, aunque rara vez querrás más de cuatro o cinco.

Controlar los cortes con break-*

Si metes tarjetas en columnas, verás que se parten entre una columna y la siguiente. Para evitarlo está break-inside-avoid:

<div class="columns-3 gap-3">
  <div class="mb-3 break-inside-avoid rounded bg-indigo-200 p-2">Ratón inalámbrico</div>
  <div class="mb-3 break-inside-avoid rounded bg-orange-200 p-2">Portátil UltraBook 14</div>
  <!-- ... -->
</div>

Dos composiciones de tarjetas en tres columnas. En la izquierda una tarjeta se parte entre columnas; en la derecha ninguna

Figura 14. A la izquierda, la tarjeta naranja continúa arriba de la segunda columna. A la derecha, break-inside-avoid lo impide.

Esta es la forma más sencilla de lograr un diseño de mampostería (masonry), con tarjetas de distinta altura encajadas. Ten en cuenta su límite: el orden visual va de arriba abajo y columna a columna, no por filas. Sirve para opiniones o fotos, pero no para un catálogo donde importe el orden.

Usa márgenes (mb-3) para separar las tarjetas verticalmente: gap-* solo actúa entre columnas.

Las clases break-* también controlan los saltos de página al imprimir:

Clase CSS Uso
break-inside-avoid break-inside: avoid No partir este bloque
break-inside-avoid-column break-inside: avoid-column Solo entre columnas
break-before-column break-before: column Empezar en columna nueva
break-after-column break-after: column Forzar corte después
break-before-page, break-after-page break-before: page… Salto de página al imprimir

box-decoration-*

box-decoration-* decide qué pasa con el fondo y el relleno de un elemento que se parte, ya sea entre columnas o entre líneas.

<span class="box-decoration-clone rounded-lg bg-indigo-600 px-3 leading-7 font-bold text-white">
  Portátil Gaming 16 con gráfica dedicada
</span>

Un texto a varias columnas y dos títulos partidos en dos líneas: con slice el fondo se corta recto; con clone cada línea tiene su relleno y sus esquinas

Figura 15. Arriba, columns-3xs deja dos columnas en 812 px. Abajo, box-decoration-slice frente a box-decoration-clone.

box-decoration-slice es el valor por defecto: el elemento se pinta como una sola tira cortada en trozos. box-decoration-clone da a cada trozo su relleno y sus esquinas redondeadas.


8. aspect-*: proporción de una caja

aspect-ratio fija la relación entre ancho y alto: das una de las dos medidas y el navegador calcula la otra. Reserva el sitio antes de que cargue una imagen, lo que evita que la página salte.

Clase CSS generado (v4)
aspect-square aspect-ratio: 1 / 1
aspect-video aspect-ratio: var(--aspect-video), que vale 16 / 9
aspect-auto aspect-ratio: auto
aspect-[4/3] aspect-ratio: 4/3

Proporciones con fracción 🆕 v4.0

🆕 Desde Tailwind 4.0. No existe en la LTS (Tailwind 3.4), que solo trae aspect-square, aspect-video y aspect-auto. Si usas la LTS: escribe el valor arbitrario, por ejemplo aspect-[3/2] en lugar de aspect-3/2, que funciona igual en las dos versiones.

En v4 cualquier fracción es una clase: aspect-3/2 genera aspect-ratio: 3/2.

<img src="/foto.svg" alt="Portátil" class="aspect-3/2 w-full object-cover">

Cuatro cajas con proporciones cuadrada, 16:9, 3:2 y 3:4. Debajo, dos cajas 16:9 con doce líneas de texto: una crece y la otra recorta

Figura 16. Las proporciones habituales, y qué pasa cuando el contenido no cabe.

Tres comportamientos que conviene conocer, medidos en un navegador real:

  • El contenido gana. Una caja aspect-video con demasiado texto crece y pierde la proporción (naranja). Para mantenerla, añade overflow-hidden, que cambia la altura mínima automática (azul).
  • El ancho y el alto explícitos ganan. aspect-video w-64 h-20 midió 256 × 80 px: si das las dos medidas, no queda nada que calcular.
  • aspect-auto devuelve a la imagen su proporción natural. La foto de 480 × 320 con w-64 aspect-auto midió 256 × 170,7 px. Con aspect-square midió 256 × 256 y, sin object-*, se habría deformado.

Proporciones propias 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) las proporciones se definen en tailwind.config.js. Si usas la LTS: escribe theme: { extend: { aspectRatio: { retro: '4 / 3' } } } y tendrás aspect-retro.

@theme {
  --aspect-retro: 4 / 3;
}

Esto genera aspect-retro, con aspect-ratio: var(--aspect-retro).


9. object-*: cómo llena una imagen su caja

Cuando das a una imagen un tamaño distinto al suyo, hay que decidir cómo se adapta. object-fit funciona en elementos reemplazados (img, video), no en un div.

Clase CSS Qué hace
object-fill object-fit: fill Estira la imagen hasta llenar la caja (valor por defecto)
object-contain object-fit: contain La muestra entera y deja bandas
object-cover object-fit: cover Llena la caja y recorta lo que sobra
object-none object-fit: none Tamaño natural, centrada y recortada
object-scale-down object-fit: scale-down Como contain, pero nunca amplía

Dos filas de la misma foto en cuadrados. Arriba, los cinco modos de object-fit: fill deforma el círculo central. Abajo, object-cover con distintas posiciones

Figura 17. Arriba, los cinco valores de object-fit. Abajo, cómo object-position decide qué parte se recorta.

Fíjate en el círculo de la E. Con object-fill se convierte en un óvalo: es lo que ocurre en cualquier imagen a la que fuerzas tamaño sin añadir object-cover. Recuerda esta regla: foto de ambiente, object-cover; logotipo o producto que debe verse entero, object-contain.

object-position

object-cover centra por defecto. Si lo importante de la foto no está en el centro, mueve el encuadre:

Clase CSS
object-center, object-top, object-bottom, object-left, object-right object-position: top…
object-left-top, object-right-bottom… object-position: left top…
object-[25%_50%] object-position: 25% 50%
object-(--foco) (v4) object-position: var(--foco)

En la segunda fila de la figura, la foto se recorta a lo ancho, de modo que object-left conserva las esquinas A y C, y object-right las B y D. El porcentaje es muy útil cuando el encuadre depende de cada foto: object-[25%_50%] deja ver un poco más de la izquierda.

Nombres nuevos para las esquinas 🆕 v4.1

🆕 Desde Tailwind 4.1. No existe en la LTS (Tailwind 3.4). Si usas la LTS: usa object-left-top, object-right-bottom… que además siguen funcionando en 4.3.3.

v4.1 añade object-top-left, object-bottom-right, object-top-right y object-bottom-left, con el orden natural de los nombres. Generan el mismo CSS que los antiguos.


10. container 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) container se configura con theme.container (center, padding) en tailwind.config.js; en 4.0 esas opciones desaparecen y se usa @utility container. Si usas la LTS: escribe theme: { container: { center: true, padding: '1rem' } }. Los anchos son iguales, y en la 3.4 están en píxeles.

container no centra nada por sí solo, y esa es la primera sorpresa: limita el ancho al punto de ruptura más cercano que quepa.

/* Lo que genera v4 */
.container {
  width: 100%;
  @media (width >= 40rem) { max-width: 40rem; }
  @media (width >= 48rem) { max-width: 48rem; }
  @media (width >= 64rem) { max-width: 64rem; }
  @media (width >= 80rem) { max-width: 80rem; }
  @media (width >= 96rem) { max-width: 96rem; }
}

Para que quede centrado y con aire a los lados, hay que escribirlo cada vez:

<div class="container mx-auto px-4">...</div>

Estas son las medidas reales a varios anchos de ventana, junto a max-w-3xl mx-auto, que es su alternativa:

Ventana container max-w-3xl
500 px 500 500
639 px 639 639
640 px 640 640
800 px 768 768
1024 px 1024 768
1280 px 1280 768
1536 px 1536 768
1900 px 1536 768

Página de 800 px: la caja container mide 768

Figura 18. A 800 px, container mide 768: el ancho va a saltos.

Página de 1200 px: la caja container mide 1024 y la max-w-3xl sigue en 768

Figura 19. A 1200 px, container salta a 1024, mientras max-w-3xl se queda en 768.

container da saltos al cruzar cada punto de ruptura, mientras que max-w-* crece con fluidez hasta su tope. Hoy casi todos los diseños prefieren lo segundo, porque controlas el ancho real de lectura. container sigue siendo cómodo cuando quieres que toda la página se alinee con los mismos puntos de ruptura.

Configurarlo una sola vez

Repetir mx-auto px-4 cansa. En v4 extiendes la utilidad con @utility, que se suma a la generada:

@import "tailwindcss";

@utility container {
  margin-inline: auto;
  padding-inline: 1rem;
}

El equivalente en v3 está en tailwind.config.js:

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./*.html'],
  theme: {
    container: {
      center: true,
      padding: '1rem',
    },
  },
};

Compilé ambos y container queda igual: margin automático, padding de 1 rem y los mismos anchos.

Los anchos se toman del tema. Si cambias --breakpoint-sm a 30rem o añades --breakpoint-3xl: 120rem en @theme, comprobé que container los recoge sin nada más. Lo verás en el módulo 10.

Otras dos notas:

  • v3 lo pone en la capa components y v4 en utilities. Podrías temer que container max-w-md cambie, pero en la prueba max-w-md ganó en las dos versiones, con 448 px.
  • No lo confundas con @container. La clase container limita el ancho; @container activa las consultas de contenedor del módulo 10. Se parecen solo en el nombre.

11. scroll-*: afinar el desplazamiento

Desplazamiento suave

Clase CSS
scroll-smooth scroll-behavior: smooth
scroll-auto scroll-behavior: auto

Se pone en <html> para que los enlaces de ancla se animen. Hazlo condicional, porque a algunas personas el movimiento les causa malestar:

<html lang="es" class="motion-safe:scroll-smooth">

Anclas bajo una cabecera sticky

Hay un problema que aparece en cuanto tienes una cabecera pegajosa: al pulsar un enlace #sobremesa, el navegador coloca la sección en el borde superior, justo debajo de la cabecera, que la tapa.

Dos ventanas con cabecera. Sin scroll-pt el título Sobremesa queda debajo de la cabecera. Con scroll-pt-20 aparece debajo con margen

Figura 20. scroll-pt-* reserva una franja superior al colocar los destinos.

Hay dos clases que lo arreglan, según dónde prefieras escribirlo:

Se pone en Clases CSS
El contenedor (html) scroll-p-*, scroll-pt-*, scroll-px-*, scroll-ps-*… scroll-padding-*
El destino (section) scroll-m-*, scroll-mt-*, scroll-mx-*, scroll-ms-*… scroll-margin-*
<!-- Una vez, para todas las anclas -->
<html class="scroll-pt-20">

<!-- O en cada destino -->
<section id="sobremesa" class="scroll-mt-20">

En la tienda de este módulo, con una cabecera de 65 px de alto, la sección quedó a 0 px del borde sin ninguna clase (tapada), y a 80 px con cualquiera de las dos, unos 15 px por debajo de la cabecera.

Prefiere scroll-pt-* en html si todas las anclas son iguales, y scroll-mt-* si solo algunas necesitan un ajuste.

💡 Consejo: Estas clases usan la escala de espaciado, y su valor debería ser algo mayor que la altura de la cabecera. Si esa altura cambia con el ancho, combínalas con variantes, como en scroll-pt-16 lg:scroll-pt-24.


12. snap-*: carruseles sin JavaScript

Scroll snap hace que el desplazamiento acabe siempre alineado con un elemento. Son dos piezas: el contenedor dice en qué eje se ajusta y las hijas dicen por qué punto.

En el contenedor CSS
snap-x, snap-y, snap-both scroll-snap-type: x var(--tw-scroll-snap-strictness)…
snap-none scroll-snap-type: none
snap-mandatory Siempre acaba ajustado
snap-proximity Se ajusta solo si queda cerca (valor por defecto)
En las hijas CSS
snap-start, snap-center, snap-end scroll-snap-align: start…
snap-align-none scroll-snap-align: none
snap-normal scroll-snap-stop: normal
snap-always scroll-snap-stop: always (no se puede saltar esta hija)

snap-mandatory no hace nada solo: modifica a snap-x o snap-y a través de una variable.

<div class="flex snap-x snap-mandatory gap-4 overflow-x-auto rounded bg-gray-100 p-4">
  <article class="w-64 shrink-0 snap-center rounded-lg bg-white p-3 shadow">
    <img src="/foto.svg" alt="" class="aspect-3/2 w-full rounded object-cover">
    <h2 class="mt-2 font-bold">Portátil UltraBook 14</h2>
    <p class="text-indigo-700">899 €</p>
  </article>
  <!-- ...más tarjetas... -->
</div>

shrink-0 impide que Flexbox encoja las tarjetas para que quepan. Sin él no habría desplazamiento alguno.

Dos carruseles. En el primero la tarjeta central queda exactamente centrada; en el segundo la primera tarjeta visible se alinea a un rem del borde

Figura 21. Tras girar la rueda, el primer carrusel se recoloca hasta centrar una tarjeta y el segundo se alinea con el relleno.

Lo comprobé con la rueda: un desplazamiento de 170 px en el primer carrusel acabó en 212, con la tercera tarjeta en el centro exacto (desviación de 0 px).

scroll-px-* es necesario en cuanto hay relleno. Con snap-start, la tarjeta se alinearía con el borde del contenedor, pegada a él. scroll-px-4 retrasa ese punto para que coincida con el p-4:

<div class="flex snap-x snap-mandatory scroll-px-4 gap-4 overflow-x-auto p-4">
  <article class="w-64 shrink-0 snap-start ...">Oferta 1</article>
</div>

⚠️ Importante: snap-mandatory puede dejar contenido inaccesible si un elemento es más alto o más ancho que el contenedor, porque cada ajuste devuelve al usuario a un punto de alineación. Úsalo con tarjetas más pequeñas que el carrusel. Para secciones de altura variable, mejor snap-proximity.


13. Barras de desplazamiento con scrollbar-* 🆕 v4.3

🆕 Desde Tailwind 4.3. No existe en la LTS (Tailwind 3.4). Si usas la LTS: las propiedades son CSS estándar, así que escribe valores arbitrarios: [scrollbar-width:thin], [scrollbar-color:#6366f1_#e0e7ff] y [scrollbar-gutter:stable]. Pierdes los colores del tema y los modificadores de opacidad.

Hasta ahora había que escribir CSS propio para dar estilo a las barras. La 4.3 añade tres familias de utilidades que traducen tres propiedades CSS. Comprobé con las versiones históricas que ninguna genera CSS en 4.2.

Clase CSS generado
scrollbar-auto scrollbar-width: auto
scrollbar-thin scrollbar-width: thin
scrollbar-none scrollbar-width: none
scrollbar-thumb-indigo-500 scrollbar-color con ese color para el mango
scrollbar-track-gray-100 scrollbar-color con ese color para el carril
scrollbar-gutter-auto scrollbar-gutter: auto
scrollbar-gutter-stable scrollbar-gutter: stable
scrollbar-gutter-both scrollbar-gutter: stable both-edges
<div class="h-40 overflow-y-scroll scrollbar-thin scrollbar-thumb-indigo-500 scrollbar-track-indigo-100
            dark:scrollbar-thumb-indigo-300 dark:scrollbar-track-gray-800">
  ...
</div>

Cuatro listas con barra normal, estrecha, sin barra y con colores de la marca. Debajo, dos pares de cajas: con scrollbar-gutter-stable, la caja corta conserva el mismo ancho que la larga

Figura 22. Las barras de Chromium en Windows con cada clase, y la diferencia que aporta scrollbar-gutter-stable.

La misma figura en modo oscuro, con el mango claro sobre un carril gris oscuro

Figura 23. Con dark:scrollbar-thumb-* y dark:scrollbar-track-* la barra se integra en el modo oscuro.

Puntos a tener en cuenta:

  • Pon siempre los dos colores. Si solo das uno, el otro queda transparente. Con scrollbar-thumb-indigo-500 a secas, el carril computado fue rgba(0, 0, 0, 0).
  • scrollbar-color se hereda. Una sola clase en <html> o en un panel afecta a todos los desplazamientos de dentro. Comprobado con una hija: recibió el mismo color.
  • scrollbar-thin no es una medida. El navegador decide qué significa estrecho, y el sistema operativo también influye. No puedes pedir 4 px.
  • scrollbar-none no impide desplazar. Solo oculta la barra: la rueda, el dedo y el teclado siguen funcionando. Pero quien no vea la barra necesita otra pista de que hay más contenido, como una tarjeta cortada en el borde.

Compatibilidad

Tailwind genera el CSS, pero quien lo interpreta es el navegador. Según los datos de compatibilidad de MDN:

Propiedad Chrome Firefox Safari
scrollbar-width 121 64 18.2
scrollbar-color 121 64 26.2
scrollbar-gutter 94 97 18.2

v4 admite desde Safari 16.4, así que esas clases son mejora progresiva: en un Safari antiguo la barra se queda como estaba y nada se rompe. Si quieres ocultar la barra también en esos Safari, añade la variante arbitraria [&::-webkit-scrollbar]:hidden, que se genera igual en las dos versiones.

Evitar el salto de la página con scrollbar-gutter

En Windows y en muchos Linux, la barra ocupa sitio. Cuando aparece o desaparece, todo el contenido cambia de ancho. Pasa al abrir una ventana modal, si bloqueas el fondo con overflow-hidden en <html>, o al navegar entre una página corta y una larga.

scrollbar-gutter-stable reserva la franja de la barra aunque no se vea. Lo medí en la tienda, a 1000 px de ancho, con el panel de la cesta abierto y html con overflow-hidden:

  Cabecera con el panel cerrado Cabecera con el panel abierto Desplazamiento del título
Sin scrollbar-gutter-stable 985 px 1000 px 7,5 px
Con scrollbar-gutter-stable 985 px 985 px 0 px

Una advertencia si quieres repetir la medida: document.documentElement.clientWidth no sirve. Con el gutter devolvía 1000 aunque el contenido siguiera midiendo 985. Mide siempre las cajas reales.

💡 Consejo: Un fixed inset-0 no cubre la franja reservada. Por eso en la figura 27 se ve una tira a la derecha del panel. Si eso molesta, da a <html> el mismo color de fondo que a la capa.


14. Caso completo: la tienda

Vamos a reunir casi todo el módulo en la estructura de una página real. Fíjate en que cada clase de layout responde a un problema que ya has visto:

<!doctype html>
<html lang="es" class="scroll-pt-20 scrollbar-gutter-stable motion-safe:scroll-smooth dark:scheme-dark">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>EMPRESA · Tienda</title>
  <link rel="stylesheet" href="/app.css">
</head>
<body class="bg-gray-50 text-gray-800 dark:bg-gray-950 dark:text-gray-100">

  <!-- Enlace de salto: oculto hasta que recibe el foco -->
  <a href="#catalogo" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:rounded focus:bg-white focus:px-4 focus:py-2 focus:text-indigo-700">Saltar al catálogo</a>

  <!-- Cabecera pegajosa -->
  <header class="sticky top-0 z-30 border-b border-gray-200 bg-white/90 backdrop-blur dark:border-gray-800 dark:bg-gray-900/90">
    <div class="container mx-auto flex h-16 items-center justify-between px-4">
      <a href="#" class="text-xl font-bold text-indigo-700 dark:text-indigo-300">EMPRESA</a>
      <nav class="hidden gap-6 text-sm sm:flex">
        <a href="#portatiles">Portátiles</a>
        <a href="#sobremesa">Sobremesa</a>
        <a href="#envios">Envíos</a>
      </nav>
      <button id="abrir" class="relative rounded-lg bg-indigo-600 px-3 py-2 text-sm font-bold text-white">
        Cesta
        <span class="absolute -top-2 -right-2 flex size-5 items-center justify-center rounded-full bg-red-600 text-xs">2</span>
      </button>
    </div>
  </header>

  <!-- Portada: imagen de fondo real con texto encima -->
  <section class="relative isolate overflow-hidden">
    <img src="/foto.svg" alt="" class="absolute inset-0 -z-10 size-full object-cover object-center">
    <div class="absolute inset-0 -z-10 bg-indigo-950/60"></div>
    <div class="container mx-auto px-4 py-16 text-white">
      <h1 class="text-3xl font-bold">Rebajas de otoño</h1>
      <p class="mt-2">Hasta un 15 % en portátiles seleccionados.</p>
    </div>
  </section>

  <!-- Contenido: lateral pegajoso y catálogo -->
  <div class="container mx-auto grid gap-8 px-4 py-8 lg:grid-cols-[14rem_1fr]">
    <aside class="hidden self-start lg:sticky lg:top-20 lg:block">
      <h2 class="font-bold">Categorías</h2>
      <ul class="mt-2 space-y-1 text-sm">
        <li><a href="#portatiles">Portátiles</a></li>
        <li><a href="#sobremesa">Sobremesa</a></li>
        <li><a href="#envios">Envíos</a></li>
      </ul>
    </aside>

    <main id="catalogo" class="min-w-0 space-y-10">
      <section id="portatiles">
        <h2 class="mb-3 text-xl font-bold">Portátiles</h2>
        <div class="-mx-4 flex snap-x snap-mandatory scroll-px-4 gap-4 overflow-x-auto px-4 pb-2 scrollbar-thin">
          <article class="relative w-60 shrink-0 snap-start overflow-hidden rounded-xl bg-white shadow dark:bg-gray-900">
            <img src="/foto.svg" alt="Portátil UltraBook 14" class="aspect-3/2 w-full object-cover">
            <span class="absolute top-2 left-2 rounded-full bg-indigo-600 px-2 py-0.5 text-xs font-bold text-white">Novedad</span>
            <div class="p-3">
              <h3 class="font-bold">Portátil UltraBook 14</h3>
              <p class="text-indigo-700 dark:text-indigo-300">899 €</p>
            </div>
          </article>
          <!-- ...más tarjetas iguales... -->
        </div>
      </section>

      <section id="sobremesa">
        <h2 class="mb-3 text-xl font-bold">Sobremesa</h2>
        <div class="columns-2xs gap-4">
          <article class="mb-4 break-inside-avoid rounded-xl bg-white p-4 shadow dark:bg-gray-900">
            <h3 class="font-bold">Torre Oficina i5</h3>
            <p class="text-sm">Ordenador de sobremesa para oficina. 649 €</p>
          </article>
          <article class="mb-4 break-inside-avoid rounded-xl bg-white p-4 shadow dark:bg-gray-900">
            <h3 class="font-bold">Torre Workstation</h3>
            <p class="text-sm">Estación de trabajo para diseño y CAD. Solo quedan 4 unidades. 1899 €</p>
          </article>
        </div>
      </section>

      <section id="envios" class="h-[28rem]">
        <h2 class="mb-3 text-xl font-bold">Envíos</h2>
        <p class="text-sm">Los pedidos realizados antes de las 16:00 salen el mismo día.</p>
      </section>
    </main>
  </div>

  <!-- Panel lateral de la cesta: fuera de la cabecera -->
  <div id="panel" hidden class="fixed inset-0 z-40">
    <div id="fondo" class="absolute inset-0 bg-gray-900/50"></div>
    <aside class="absolute inset-y-0 right-0 w-80 max-w-full overflow-y-auto overscroll-contain bg-white p-5 shadow-xl dark:bg-gray-900">
      <div class="flex items-center justify-between">
        <h2 class="text-lg font-bold">Tu cesta</h2>
        <button id="cerrar" class="rounded bg-gray-200 px-2 py-1 text-sm dark:bg-gray-700">Cerrar</button>
      </div>
      <ul class="mt-4 space-y-2 text-sm">
        <li>Portátil UltraBook 14 · 899 €</li>
        <li>Ratón inalámbrico · 19,90 €</li>
      </ul>
    </aside>
  </div>

  <script>
    const panel = document.querySelector('#panel');
    const raiz = document.documentElement;
    function mostrar(abierto) {
      panel.hidden = !abierto;
      raiz.classList.toggle('overflow-hidden', abierto);
    }
    document.querySelector('#abrir').addEventListener('click', () => mostrar(true));
    document.querySelector('#cerrar').addEventListener('click', () => mostrar(false));
    document.querySelector('#fondo').addEventListener('click', () => mostrar(false));
  </script>
</body>
</html>

Página de la tienda con cabecera, portada con foto oscurecida, lateral de categorías, carrusel de portátiles y tarjetas de sobremesa

Figura 24. La tienda a 1100 px.

La tienda tras pulsar el enlace Sobremesa: la cabecera sigue arriba, el lateral también y el título queda visible

Figura 25. Tras pulsar Sobremesa: cabecera y lateral pegados, y el destino a la vista gracias a scroll-pt-20.

La tienda en un móvil de 390 px: sin lateral ni enlaces de navegación, y con el carrusel de portátiles cortado en el borde

Figura 26. A 390 px, hidden retira el menú y el lateral. La tarjeta cortada avisa de que hay más.

Decisiones del caso

Pieza Clases Por qué
Cabecera sticky top-0 z-30 Sticky conserva su sitio en el flujo; no hay que compensar su altura
Insignia de la cesta relative en el botón, absolute -top-2 -right-2 Sobresale del botón
Portada relative isolate + -z-10 en foto y capa Las capas quedan sobre el fondo y no se pierden
Lateral lg:sticky lg:top-20 self-start Sin self-start se estiraría y no se pegaría
Anclas scroll-pt-20 en html Evita que la cabecera tape el destino
Carrusel -mx-4 px-4 scroll-px-4 Llega a los bordes, conservando la alineación
Catálogo min-w-0 en main Evita que el carrusel ensanche toda la página
Panel fixed inset-0 z-40, fuera de header backdrop-blur lo habría atrapado
Bloqueo del fondo overflow-hidden en html + scrollbar-gutter-stable La página no se mueve al abrir
Lista del panel overflow-y-auto overscroll-contain Al llegar al final no arrastra la página

La línea de min-w-0 tiene historia. Mi primera versión no lo llevaba, y la página completa tenía una barra de desplazamiento horizontal. Una celda de rejilla tiene por defecto min-width: auto, y no se encoge por debajo del ancho mínimo de su contenido. Como las tarjetas del carrusel llevan shrink-0, ese mínimo era enorme. Con min-w-0 la celda obedece al 1fr, y solo se desplaza el carrusel. Es uno de los errores más frecuentes al meter un carrusel en una rejilla.

Este es el panel abierto en modo oscuro, con la tira de la franja reservada a la derecha:

Panel de la cesta abierto a la derecha, sobre una capa que oscurece la tienda en modo oscuro

Figura 27. El panel fixed cubre la cabecera sticky gracias a z-40 frente a z-30.

💡 Consejo: La página usa un div con fixed para practicar el posicionamiento. El módulo 17 presenta <dialog>, que resuelve de forma nativa el foco y la tecla Escape. Para una ventana real, usa esa opción.

Si compilas el caso en la LTS

En v3 todo el esqueleto funciona. Estas son las únicas clases que debes cambiar:

v4.3 v3.4
aspect-3/2 aspect-[3/2]
scrollbar-thin [scrollbar-width:thin]
scrollbar-gutter-stable [scrollbar-gutter:stable]
dark:scheme-dark dark:[color-scheme:dark]
<div id="panel" hidden class="fixed ..."> Igual: fixed no toca display, así que el atributo funciona

15. Depuración de layout

Cuando algo no se coloca donde esperas, sigue un orden. Abre las herramientas de desarrollo y pon el elemento en el panel de estilos calculados: comprueba position, display, overflow y z-index antes de cambiar nada.

Síntoma Causa probable Solución
absolute aparece en una esquina de la página Falta relative en el padre Añade relative
top-* o z-* no hacen nada La caja es static Añade relative
sticky no se pega Falta top-*, hay overflow en un ancestro, o no le queda recorrido Tabla de la sección 3
fixed no se queda fijo o mide poco Ancestro con transform, filter o backdrop-* Saca el panel de ese ancestro
z-9999 sigue debajo Atrapado en un contexto de apilamiento Busca el ancestro que lo crea; usa isolate
Un -z-10 desaparece Se ha ido detrás del fondo de un ancestro isolate en la sección
Barra horizontal en toda la página Hijo más ancho que la ventana; celda de rejilla sin min-w-0 Añade min-w-0, u overflow-x-auto en el contenedor correcto
Un menú queda cortado Ancestro con overflow distinto de visible Muévelo fuera o quita el overflow
La imagen se ve deformada Falta object-cover Añádelo
classList.add('hidden') no oculta Tiene inline-*, table o list-item en v4 Quita esa clase o usa el atributo hidden
El padre se colapsa con una imagen flotante No contiene sus floats flow-root
El contenido salta al abrir un modal Desaparece la barra scrollbar-gutter-stable

💡 Consejo: Un truco para encontrar la caja que causa una barra horizontal es añadir temporalmente **:outline **:outline-red-500 al body (en v3, [&_*]:outline [&_*]:outline-red-500). La variante **: se estudia en el módulo 11. El contorno no ocupa sitio, de modo que no altera la maqueta.


16. Ejercicios

Ejercicio 1: Insignias de stock

Crea dos tarjetas de 14 rem de ancho. La primera, para la Torre Workstation (1899 €), lleva una etiqueta “Quedan 4” abajo a la derecha y un punto rojo que sobresale de la esquina superior izquierda. La segunda, para el Portátil Gaming 16 (1499 €), lleva una cinta diagonal “Oferta” en la esquina superior derecha, recortada por la tarjeta.

Ver solución
<div class="flex gap-6">
  <article class="relative w-56 rounded-xl bg-white p-4 shadow">
    <h2 class="font-bold">Torre Workstation</h2>
    <p class="text-sm">1899 €</p>
    <span class="absolute right-2 bottom-2 rounded bg-amber-500 px-2 py-0.5 text-xs font-bold text-white">Quedan 4</span>
    <span class="absolute -top-2 -left-2 size-4 rounded-full bg-red-600"></span>
  </article>

  <article class="relative w-56 overflow-hidden rounded-xl bg-white p-4 shadow">
    <h2 class="font-bold">Portátil Gaming 16</h2>
    <p class="text-sm">1499 €</p>
    <span class="absolute top-3 -right-8 w-28 rotate-45 bg-red-600 text-center text-xs font-bold text-white">Oferta</span>
  </article>
</div>

La primera tarjeta no puede llevar overflow-hidden, porque recortaría el punto. La segunda lo necesita para cortar la cinta. La posición de la cinta se obtiene por tanteo: -right-8 la saca de la tarjeta y rotate-45 la inclina; el recorte hace el resto. Verás el resultado en la parte superior de la figura 29.

Ejercicio 2: Resumen de pedido pegajoso

Haz una página con dos columnas (grid-cols-[12rem_1fr]): a la izquierda, un resumen del pedido 5001; a la derecha, contenido muy largo. El resumen debe quedarse a 1 rem del borde superior al desplazar.

Ver solución
<div class="mx-auto grid max-w-3xl grid-cols-[12rem_1fr] gap-6 p-6">
  <aside class="sticky top-4 self-start rounded bg-indigo-100 p-3">
    <p class="font-bold">Pedido 5001</p>
    <ul class="text-sm">
      <li>Datos</li>
      <li>Envío</li>
      <li>Pago</li>
    </ul>
  </aside>
  <main class="h-[1500px] rounded bg-gray-100 p-3">Contenido largo</main>
</div>

La clave es self-start. Medido al desplazar: con esa clase, el aside mide 108 px y queda a 16 px del borde. Sin ella mide 1500 px, ocupa toda la fila y se va con la página (y = −428 px).

Ejercicio 3: Cuatro sticky rotos

Di por qué ninguno de estos encabezados se pega al desplazar la ventana, y cómo se arregla cada uno:

<!-- A -->
<header class="sticky bg-white">Tienda</header>

<!-- B -->
<div class="overflow-hidden">
  <header class="sticky top-0 bg-white">Tienda</header>
  <main>...</main>
</div>

<!-- C -->
<div>
  <header class="sticky top-0 bg-white">Tienda</header>
</div>
<main>...</main>

<!-- D -->
<div class="flex">
  <nav class="sticky top-0 w-48">Menú</nav>
  <main class="h-[2000px]">...</main>
</div>
Ver solución
  • A: falta el umbral. Sin top-* no se pega: añade top-0.
  • B: overflow-hidden en el padre lo convierte en su contenedor de desplazamiento, y este no se desplaza. Cambia a overflow-clip o quítalo.
  • C: el div envoltorio mide lo mismo que el header, y sticky no puede salir de su padre. En la prueba, tras desplazar 100 px, la cabecera se fue los 100 px enteros. Quita el div, o pon el sticky en el envoltorio.
  • D: en una fila flex las hijas se estiran hasta la altura de la fila, igual que en una rejilla. Añade self-start al nav.

Ejercicio 4: El menú tapado

Este menú queda debajo del contenido. Arréglalo de dos maneras, y explica por qué z-9999 no funcionaría:

<header class="relative z-0">
  Cabecera
  <div class="absolute top-8 left-2 z-50">Menú</div>
</header>
<main class="relative z-10">Contenido</main>
Ver solución

relative z-0 crea un contexto de apilamiento. Dentro de él, el menú tiene z-50, pero la cabecera entera vale 0 frente al main, que vale 10. Por eso z-9999 no cambia nada: compite en otro grupo.

<!-- Opción 1: quitar el contexto. Sin z-*, el menú compite arriba con su z-50 -->
<header class="relative">...</header>

<!-- Opción 2: subir el grupo entero por encima del main -->
<header class="relative z-20">...</header>

Medí con elementFromPoint la zona de solape: con el código original devuelve el contenido, y con cualquiera de los dos arreglos devuelve el menú. En un proyecto real es preferible la opción 2, porque la cabecera debe estar por encima del contenido en todo momento.

Ejercicio 5: Ventana modal centrada

Crea una ventana modal de confirmación que cubra toda la pantalla con una capa oscura, con una caja centrada. La caja no debe superar el 80 % de la altura ni el 90 % del ancho, y si su contenido no cabe, debe desplazarse sin arrastrar la página.

Ver solución
<div class="fixed inset-0 z-50">
  <div class="absolute inset-0 bg-gray-900/60"></div>
  <div role="dialog" aria-modal="true" aria-labelledby="titulo"
       class="absolute top-1/2 left-1/2 max-h-[80dvh] w-96 max-w-[90vw] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-xl bg-white p-6 shadow-xl">
    <h2 id="titulo" class="text-lg font-bold">Eliminar del pedido</h2>
    <p class="mt-2 text-sm">¿Quieres quitar el Portátil Gaming 16 de tu pedido? Esta acción no se puede deshacer.</p>
    <div class="mt-4 text-right">
      <button class="rounded bg-gray-200 px-3 py-1">Cancelar</button>
      <button class="ml-2 rounded bg-red-600 px-3 py-1 text-white">Eliminar</button>
    </div>
  </div>
</div>

Ventana modal centrada sobre una capa oscura que cubre la página

Figura 28. La ventana modal. En una ventana de 900 × 700 px, su centro cae en (442,5, 350): el centro del área sin barra.

La altura de la caja depende del texto, por lo que se centra con translate, que funciona sin conocer su tamaño. La capa y la caja son hermanas dentro del fixed: la capa cubre todo con inset-0 y la caja se pinta encima por ir después en el HTML.

Ejercicio 6: Galería con encuadres

Monta una galería de tres columnas con la misma foto, en cuadrados, cada una con un encuadre distinto: la foto entera recortada por el centro, un detalle de la izquierda y un detalle de la pantalla a tamaño natural. Añade un pie sobre cada imagen, pegado a la parte inferior.

Ver solución
<div class="grid max-w-md grid-cols-3 gap-2">
  <figure class="relative">
    <img src="/foto.svg" alt="Portátil" class="aspect-square w-full rounded object-cover">
    <figcaption class="absolute inset-x-0 bottom-0 rounded-b bg-gray-900/60 px-1 text-center text-xs text-white">Portátil</figcaption>
  </figure>
  <figure class="relative">
    <img src="/foto.svg" alt="Detalle de la esquina" class="aspect-square w-full rounded object-cover object-left">
    <figcaption class="absolute inset-x-0 bottom-0 rounded-b bg-gray-900/60 px-1 text-center text-xs text-white">Esquina</figcaption>
  </figure>
  <figure class="relative">
    <img src="/foto.svg" alt="Detalle de la pantalla" class="aspect-square w-full rounded object-none">
    <figcaption class="absolute inset-x-0 bottom-0 rounded-b bg-gray-900/60 px-1 text-center text-xs text-white">Pantalla</figcaption>
  </figure>
</div>

object-none conserva el tamaño natural y centra la imagen: es un zoom barato, sin escribir ningún factor. En v3 todo funciona igual.

Dos tarjetas con insignias, tres fotos con pies y una lista de opiniones en dos columnas

Figura 29. Resultado de los ejercicios 1, 6 y 7.

Ejercicio 7: Mampostería de opiniones

Coloca seis opiniones de clientes, de longitudes muy distintas, en una composición de mampostería que elija el número de columnas según el ancho disponible. Ninguna tarjeta debe partirse.

Ver solución
<div class="columns-3xs gap-4">
  <blockquote class="mb-4 break-inside-avoid rounded-lg bg-white p-3 text-sm shadow">
    Llegó en 24 horas y bien embalado.
  </blockquote>
  <blockquote class="mb-4 break-inside-avoid rounded-lg bg-white p-3 text-sm shadow">
    El UltraBook pesa poquísimo, lo llevo a clase todos los días. La batería aguanta la jornada
    entera y la pantalla no da reflejos. Muy contento con la compra.
  </blockquote>
  <!-- ...cuatro más iguales... -->
</div>

columns-3xs adapta las columnas sin puntos de ruptura. La separación vertical la pone mb-4, ya que gap-4 solo separa columnas. Recuerda que el orden es por columnas, algo aceptable aquí, porque las opiniones no siguen una secuencia.

Ejercicio 8: Carrusel de categorías, también en v3

Crea una fila de categorías en forma de píldoras con desplazamiento horizontal: sin barra visible, con ajuste a la izquierda respetando el relleno. Escribe la versión v4 y la equivalente para la LTS.

Ver solución
<!-- v4.3 -->
<div class="flex snap-x snap-mandatory scroll-px-4 scrollbar-none gap-3 overflow-x-auto rounded bg-white p-4">
  <a href="#" class="shrink-0 snap-start rounded-full bg-indigo-600 px-4 py-2 text-sm font-bold text-white">Todo</a>
  <a href="#" class="shrink-0 snap-start rounded-full bg-gray-200 px-4 py-2 text-sm">Portátiles</a>
  <a href="#" class="shrink-0 snap-start rounded-full bg-gray-200 px-4 py-2 text-sm">Sobremesa</a>
  <!-- ...más categorías... -->
</div>

<!-- v3.4: solo cambia la clase de la barra -->
<div class="flex snap-x snap-mandatory scroll-px-4 gap-3 overflow-x-auto rounded bg-white p-4 [scrollbar-width:none]">
  ...
</div>

Dos filas de píldoras de categorías que se cortan en el borde derecho, sin barra de desplazamiento

Figura 30. La primera fila, desplazada; la segunda, en reposo. Ninguna muestra barra.

Medido a 420 px de ancho: tras girar la rueda 120 px el carrusel acabó en 77 px, con la segunda píldora justo a 16 px del borde, el valor de scroll-px-4. La barra no ocupa alto (0 px de diferencia entre offsetHeight y clientHeight). La píldora cortada en el borde es la pista de que hay más contenido.

Ejercicio 9: El botón que no se oculta

Este botón de Ver cesta debe ocultarse si la cesta está vacía, pero en v4 sigue visible. Explica la causa y da una solución para v4 y otra que funcione igual en v3 y en v4.

<a id="ver" class="inline-flex items-center gap-2">Ver cesta</a>
<script>
  const cestaVacia = true;
  if (cestaVacia) document.querySelector('#ver').classList.add('hidden');
</script>
Ver solución

En v4 las clases de una misma propiedad se ordenan alfabéticamente: .hidden va antes que .inline-flex, así que gana inline-flex. En v3 .hidden iba al final.

Solución para v4, sin cambiar el HTML: usar el atributo, cuya regla de Preflight lleva !important.

document.querySelector('#ver').hidden = cestaVacia;

Solución válida en ambas versiones: añadir la variante que respalda al atributo.

<a id="ver" class="inline-flex items-center gap-2 [&[hidden]]:hidden">Ver cesta</a>

Medido sobre inline-flex, inline-block y list-item, en v4 la clase no oculta y el atributo sí; en v3 es justo al revés. Con [&[hidden]]:hidden el atributo funciona en las dos.

Ejercicio 10: Traducir a la LTS

Reescribe estas clases para que funcionen en Tailwind 3.4: aspect-3/2, z-100, inset-s-4, top-17, columns-16, object-top-left, scrollbar-gutter-stable, scrollbar-thumb-indigo-500 scrollbar-track-indigo-100.

Ver solución
v4 v3.4
aspect-3/2 aspect-[3/2]
z-100 z-[100]
inset-s-4 start-4
top-17 top-[4.25rem]
columns-16 columns-[16]
object-top-left object-left-top
scrollbar-gutter-stable [scrollbar-gutter:stable]
scrollbar-thumb-indigo-500 scrollbar-track-indigo-100 [scrollbar-color:#6366f1_#e0e7ff]

Compilé la columna derecha con Tailwind 3.4.19, y todas generan CSS. En la última, v3 pide escribir los dos colores juntos en hexadecimal, porque no hay variable de color que usar sin más configuración.

Ejercicio 11 (propuesto): Portada con vídeo

Haz una portada a pantalla completa (h-dvh) con un <video> de fondo, una capa oscura y un título centrado, usando isolate, -z-10 y object-cover. Añade un enlace “Ver catálogo” abajo en el centro, y un botón flotante fixed que quede siempre por encima.

Ejercicio 12 (propuesto): Tabla de pedidos

Con los datos de la API de EMPRESA, crea una tabla de pedidos con cabecera y primera columna fijas, dentro de una caja con altura máxima. Añade una barra de desplazamiento fina con los colores de la marca y un scrollbar-gutter-stable para que las columnas no cambien de ancho al filtrar filas. Después, prepara la versión para la LTS.


Resumen

Concepto API Idea clave
Caja en el flujo block, inline, inline-block, flow-root, contents inline ignora ancho y alto
Conflictos de display hidden frente a inline-* En v4 pierde hidden; no juntes dos display
Ocultar hidden, invisible, opacity-0, sr-only Decide qué pasa con el hueco y el foco
Atributo hidden [&[hidden]]:hidden !important solo en v4
Posición relative, absolute, fixed, sticky Pregúntate siempre cuál es la referencia
Colocar inset-*, top-*, inset-s-* Escala de espaciado, fracciones y negativos
fixed atrapado transform, filter, backdrop-* en un ancestro Saca el panel fuera
sticky sticky top-0, self-start Necesita umbral, recorrido y ningún overflow en medio
Apilamiento z-*, isolate Solo se compara dentro del mismo contexto
Desbordamiento overflow-auto, overflow-clip clip recorta sin crear contenedor
Cadena de desplazamiento overscroll-contain La página no se arrastra
Texto alrededor float-left, flow-root float no sirve en hijas de flex
Columnas columns-3xs, break-inside-avoid Mampostería sencilla, orden por columnas
Proporción aspect-video, aspect-3/2 El contenido y las medidas explícitas ganan
Imagen en su caja object-cover, object-contain, object-position Sin object-*, se deforma
Ancho por saltos container mx-auto, @utility container No centra solo; a menudo es mejor max-w-*
Anclas scroll-pt-*, scroll-mt-*, scroll-smooth Compensa la cabecera pegajosa
Carrusel snap-x, snap-mandatory, snap-start, scroll-px-* Sin JavaScript, con shrink-0
Barras scrollbar-thin, scrollbar-thumb-*, scrollbar-track-* Da siempre los dos colores
Estabilidad scrollbar-gutter-stable La página no salta al abrir un panel