Saltar al contenido

CSS, de a poco

Curso de CSS

Animar a auto

Lección 36 de 37 · 5 min

Durante años, abrir un panel hasta el alto de su contenido pedía JavaScript o un truco. Ahora alcanza con una línea en :root, o con calc-size() donde haga falta.

Por qué auto no animaba

Una transición calcula valores intermedios entre dos números: de 72px a 300px pasa por 186px. Pero auto no es un número: es una palabra que el navegador resuelve recién al armar el layout. Entre 72px y auto no hay mitad, así que el alto salta de un valor al otro.

Dos gráficos de alto en el tiempo. De 72px a 300px la curva sube suave y pasa por 186px a mitad de camino. De 72px a auto la línea se queda en 72px y salta de golpe a auto: no hay valores intermedios.

Los arreglos de siempre tenían costo:

  1. Un tope grandemax-height: 1000pxAnima, pero el tiempo se reparte sobre los 1000px: si el contenido mide 300, se abre rápido y se cierra con una demora.
  2. Medir con JavaScriptscrollHeightEscribir el alto en píxeles funciona, pero hay que volver a medir cada vez que cambia el texto o el ancho.
  3. Una fila de grid0fr → 1frEs el mejor de los tres, y sigue sirviendo de respaldo (más abajo).

interpolate-size

interpolate-size: allow-keywords le da permiso al navegador para animar entre un largo y una palabra de tamaño: auto, min-content, max-content o fit-content. El valor por defecto, numeric-only, mantiene el salto de siempre.

Es opcional porque cambiarlo para todos podía romper sitios que contaban con el salto. Se hereda, así que se pone una vez en :root y vale para toda la página.

Abrí y cerrá el texto con cada técnica. La línea punteada marca el alto cerrado; la cota de la derecha sigue el alto que dibuja el navegador, cuadro a cuadro. Con nada, la misma transición salta.

Técnica

Cocina · 5 min de lectura

Cómo se hace el dulce de leche

El dulce de leche es leche con azúcar cocida a fuego lento hasta que se reduce a un tercio y toma color. Lleva una pizca de bicarbonato, que evita que se corte y ayuda a oscurecerlo.

Para un frasco de 500 g van dos litros de leche entera y 400 g de azúcar. Se revuelve con cuchara de madera, sin parar en la última media hora, cuando espesa y se pega al fondo.

Está listo cuando una gota sobre un plato frío no se corre. Si se pasa queda granuloso; con un chorrito de leche caliente y unos minutos más de fuego, se recupera.

extracto.css
:root {
interpolate-size: allow-keywords;
}
.extracto {
height: 72px;
overflow: clip;
transition: height 350ms cubic-bezier(0.22, 1, 0.36, 1);
}
.extracto[data-open] {
height: auto;
transition-duration: 400ms;
}
@media (prefers-reduced-motion: reduce) {
.extracto {
transition: none;
}
}
extracto.html
<html class="[interpolate-size:allow-keywords]">
…
<div id="excerpt" class="h-18 overflow-clip transition-[height] duration-350 ease-[cubic-bezier(0.22,1,0.36,1)] data-open:duration-400 motion-reduce:transition-none data-open:h-auto" data-open>El dulce de leche es…</div>
<button aria-expanded="true" aria-controls="excerpt">Leer menos</button>

Abre en 400ms y cierra en 350ms: cerrar un poco más rápido se siente más ágil. Solo anima entre un largo y una palabra; de auto a min-content, por ejemplo, sigue saltando.

calc-size(): cuentas con el tamaño intrínseco

calc-size() toma un tamaño intrínseco y una cuenta. Dentro de la cuenta, size vale lo que mide ese tamaño. Como el resultado es un largo, anima sin necesitar interpolate-size.

css
/* lo mismo que auto, pero animable */
.panel[data-open] {
height: calc-size(auto, size);
}
/* auto más 2rem de aire */
.aviso[data-open] {
height: calc-size(auto, size + 2rem);
}
/* la mitad de lo que mediría el contenido */
.vista-previa {
height: calc-size(max-content, size * 0.5);
}

Usá interpolate-size en :root para el caso general y calc-size() cuando necesites hacer cuentas con el tamaño. Tiene el mismo soporte: Chrome y Edge 129, sin Firefox ni Safari. Ahí la declaración entera no vale y queda el valor anterior, así que conviene poner antes un respaldo como height: auto.

Abrir un details

<details> abre y cierra solo, sin JavaScript, pero su contenido aparecía de golpe. El pseudo-elemento ::details-content es la caja que envuelve todo lo que no es el <summary>, y se le puede animar el alto.

css
:root {
interpolate-size: allow-keywords;
}
.ficha::details-content {
block-size: 0;
overflow-y: clip;
transition:
block-size 350ms cubic-bezier(0.22, 1, 0.36, 1),
content-visibility 350ms allow-discrete;
}
.ficha[open]::details-content {
block-size: auto;
transition-duration: 400ms;
}

Cerrado, el navegador oculta ese contenido con content-visibility: hidden, que cambia de una vez. Con allow-discrete, ese cambio espera al final de la transición, así el contenido se ve mientras se cierra.

Con el atributo name, los <details> que lo comparten funcionan como acordeón: abrir uno cierra el resto. La etiqueta naranja de cada fila mide su contenido en vivo.

Abrir con

Lámpara Arco · ficha técnica

4 secciones

Medidas
Alto
46 cm
Brazo
38 cm
Base
16 cm de diámetro
Peso
1,9 kg
Materiales
Base
Hierro fundido
Brazo
Aluminio
Pantalla
Chapa pintada al horno
Luz
Fuente
LED de 8 W
Temperatura
2700 K
Flujo
600 lm
Dimmer
4 pasos, en la base
Cable
1,8 m, textil
Cuidado
Limpieza
Paño seco, sin alcohol sobre la pintura
ficha.css
:root {
interpolate-size: allow-keywords;
}
.ficha::details-content {
block-size: 0;
overflow-y: clip;
transition:
block-size 350ms cubic-bezier(0.22, 1, 0.36, 1),
content-visibility 350ms allow-discrete;
}
.ficha[open]::details-content {
block-size: auto;
transition-duration: 400ms;
}
@media (prefers-reduced-motion: reduce) {
.ficha::details-content {
transition: none;
}
}
ficha.html
<html class="[interpolate-size:allow-keywords]">
…
<details class="details-content:block-0 details-content:overflow-y-clip details-content:transition-[block-size,content-visibility] details-content:transition-discrete details-content:duration-350 details-content:ease-[cubic-bezier(0.22,1,0.36,1)] open:details-content:block-auto open:details-content:duration-400 motion-reduce:details-content:transition-none" name="ficha">
<summary>Medidas</summary>
<dl>…</dl>
</details>

::details-content es Baseline 2025: Chrome 131, Firefox 143 y Safari 18.4. Pero para animar a auto también necesita interpolate-size o calc-size(), así que hoy anima en Chrome y Edge, y en Firefox y Safari abre de golpe, como siempre.

El truco de grid, como respaldo

Si necesitás que anime en todos los navegadores hoy, usá grid. Una fila de 0fr mide lo mínimo que puede medir su hijo; una de 1fr, todo su contenido. Las filas de grid se animan como cualquier número, y eso es Baseline desde 2022 (Chrome 107, Firefox 66, Safari 16).

css
.extracto {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 350ms cubic-bezier(0.22, 1, 0.36, 1);
}
.extracto[data-open] {
grid-template-rows: 1fr;
}
.extracto > div {
min-height: 0; /* o el alto cerrado, como 72px */
overflow: hidden;
}

Necesita un elemento más adentro, y el min-height de ese hijo es lo que define el alto cerrado: sin él, el hijo no se achica por debajo de su contenido y la fila no cierra.

Movimiento reducido

Animar el alto mueve todo lo que está abajo. Para quien pidió menos movimiento, sacá la transición: el panel se abre igual, solo que sin el recorrido.

css
@media (prefers-reduced-motion: reduce) {
.extracto,
.ficha::details-content {
transition: none;
}
}

Si querés conservar alguna señal, un fundido de opacity del contenido no desplaza nada y se puede dejar. Es la regla general: con movimiento reducido se queda la opacidad y el color, y se va el desplazamiento.

En Tailwind

Tailwind tiene h-auto y variantes para el estado (data-open:, open:) y para el pseudo-elemento (details-content:). Para interpolate-size y calc-size() no hay clases propias: van entre corchetes.

CSSTailwind
interpolate-size: allow-keywords[interpolate-size:allow-keywords]
height: 72px → autoh-18 data-open:h-auto
height: calc-size(auto, size)h-[calc-size(auto,size)]
transition: height 350mstransition-[height] duration-350
::details-content { block-size: 0 }details-content:block-0
transition-behavior: allow-discretetransition-discrete
grid-template-rows: 0fr → 1frgrid-rows-[0fr] data-open:grid-rows-[1fr]
sin transición con movimiento reducidomotion-reduce:transition-none
ficha.html
<html class="[interpolate-size:allow-keywords]">
…
<details name="ficha" class="
details-content:block-0
details-content:overflow-y-clip
details-content:transition-[block-size,content-visibility]
details-content:transition-discrete
details-content:duration-350
open:details-content:block-auto
open:details-content:duration-400
motion-reduce:details-content:transition-none">
<summary>Medidas</summary>
<dl>…</dl>
</details>
</html>