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.
Los arreglos de siempre tenían costo:
- Un tope grande
max-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. - Medir con JavaScript
scrollHeightEscribir el alto en píxeles funciona, pero hay que volver a medir cada vez que cambia el texto o el ancho. - Una fila de grid
0fr → 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.
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.
: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;}}
<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.
/* 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.
: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.
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
: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;}}
<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).
.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.
@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.
| CSS | Tailwind |
|---|---|
| interpolate-size: allow-keywords | [interpolate-size:allow-keywords] |
| height: 72px → auto | h-18 data-open:h-auto |
| height: calc-size(auto, size) | h-[calc-size(auto,size)] |
| transition: height 350ms | transition-[height] duration-350 |
| ::details-content { block-size: 0 } | details-content:block-0 |
| transition-behavior: allow-discrete | transition-discrete |
| grid-template-rows: 0fr → 1fr | grid-rows-[0fr] data-open:grid-rows-[1fr] |
| sin transición con movimiento reducido | motion-reduce:transition-none |
<html class="[interpolate-size:allow-keywords]">…<details name="ficha" class="details-content:block-0details-content:overflow-y-clipdetails-content:transition-[block-size,content-visibility]details-content:transition-discretedetails-content:duration-350open:details-content:block-autoopen:details-content:duration-400motion-reduce:details-content:transition-none"><summary>Medidas</summary><dl>…</dl></details></html>
Animating to auto
Lesson 36 of 37 · 5 min
For years, opening a panel to the height of its content took JavaScript or a trick. Now one line on :root does it, or calc-size() where you need it.
Why auto didn't animate
A transition works out the values in between two numbers: from 72px to 300px it passes through 186px. But auto isn't a number: it's a keyword the browser only resolves while doing layout. There's no halfway point between 72px and auto, so the height jumps from one to the other.
The usual workarounds had a cost:
- A big cap
max-height: 1000pxIt animates, but the timing is spread over the 1000px: if the content is 300px tall, it opens fast and closes after a delay. - Measuring with JavaScript
scrollHeightWriting the height in pixels works, but it has to measure again whenever the text or the width changes. - A grid row
0fr → 1frThe best of the three, and still a good fallback (see below).
interpolate-size
interpolate-size: allow-keywords lets the browser animate between a length and a sizing keyword: auto, min-content, max-content or fit-content. The default, numeric-only, keeps the old jump.
It's opt-in because changing it for everyone could break sites that relied on the jump. It's inherited, so you set it once on :root and it covers the whole page.
Open and close the text with each technique. The dashed line marks the closed height; the dimension on the right follows the height the browser draws, frame by frame. With nothing, the same transition jumps.
Cooking · 5 min read
How dulce de leche is made
Dulce de leche is milk and sugar cooked over low heat until it reduces to a third and turns golden. A pinch of baking soda keeps it from curdling and helps it darken.
A 500 g jar takes two liters of whole milk and 400 g of sugar. Stir with a wooden spoon, nonstop for the last half hour, when it thickens and starts sticking to the bottom.
It's done when a drop on a cold plate doesn't run. Overcooked, it turns grainy; a splash of hot milk and a few more minutes on the heat bring it back.
:root {interpolate-size: allow-keywords;}.excerpt {height: 72px;overflow: clip;transition: height 350ms cubic-bezier(0.22, 1, 0.36, 1);}.excerpt[data-open] {height: auto;transition-duration: 400ms;}@media (prefers-reduced-motion: reduce) {.excerpt {transition: none;}}
<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>Dulce de leche is…</div><button aria-expanded="true" aria-controls="excerpt">Read less</button>
It opens in 400ms and closes in 350ms: closing a little faster feels snappier. It only animates between a length and a keyword; from auto to min-content, say, it still jumps.
calc-size(): math on intrinsic sizes
calc-size() takes an intrinsic size and a calculation. Inside the calculation, size is whatever that size measures. Since the result is a length, it animates without interpolate-size.
/* the same as auto, but animatable */.panel[data-open] {height: calc-size(auto, size);}/* auto plus 2rem of room */.notice[data-open] {height: calc-size(auto, size + 2rem);}/* half of what the content would measure */.preview {height: calc-size(max-content, size * 0.5);}
Use interpolate-size on :root for the general case and calc-size() when you need math on the size. Support is the same: Chrome and Edge 129, no Firefox or Safari. There, the whole declaration is invalid and the previous value stays, so put a fallback like height: auto before it.
Opening a details element
<details> opens and closes on its own, no JavaScript, but its content used to appear at once. The ::details-content pseudo-element is the box around everything that isn't the <summary>, and its height can be animated.
:root {interpolate-size: allow-keywords;}.spec::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;}.spec[open]::details-content {block-size: auto;transition-duration: 400ms;}
When closed, the browser hides that content with content-visibility: hidden, which flips at once. With allow-discrete, the flip waits until the end of the transition, so the content stays visible while it closes.
With the name attribute, <details> elements that share it act as an accordion: opening one closes the rest. The orange label on each row measures its content live.
Arco lamp · spec sheet
4 sections
Dimensions
- Height
- 46 cm
- Arm
- 38 cm
- Base
- 16 cm across
- Weight
- 1.9 kg
Materials
- Base
- Cast iron
- Arm
- Aluminum
- Shade
- Powder-coated steel
Light
- Source
- 8 W LED
- Color
- 2700 K
- Output
- 600 lm
- Dimmer
- 4 steps, on the base
- Cord
- 1.8 m, fabric
Care
- Cleaning
- Dry cloth, no alcohol on the paint
:root {interpolate-size: allow-keywords;}.spec::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;}.spec[open]::details-content {block-size: auto;transition-duration: 400ms;}@media (prefers-reduced-motion: reduce) {.spec::details-content {transition: none;}}
<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="spec"><summary>Dimensions</summary><dl>…</dl></details>
::details-content is Baseline 2025: Chrome 131, Firefox 143 and Safari 18.4. But to animate to auto it also needs interpolate-size or calc-size(), so today it animates in Chrome and Edge, and Firefox and Safari open it at once, as they always did.
The grid trick, as a fallback
If you need it to animate in every browser today, use grid. A 0fr row is as short as its child can get; a 1fr row fits all of its content. Grid rows animate like any number, and that has been Baseline since 2022 (Chrome 107, Firefox 66, Safari 16).
.excerpt {display: grid;grid-template-rows: 0fr;transition: grid-template-rows 350ms cubic-bezier(0.22, 1, 0.36, 1);}.excerpt[data-open] {grid-template-rows: 1fr;}.excerpt > div {min-height: 0; /* or the closed height, like 72px */overflow: hidden;}
It needs one more element inside, and that child's min-height sets the closed height: without it, the child won't shrink below its content and the row won't close.
Reduced motion
Animating height moves everything below it. For people who asked for less motion, drop the transition: the panel still opens, just without the travel.
@media (prefers-reduced-motion: reduce) {.excerpt,.spec::details-content {transition: none;}}
If you want to keep some cue, an opacity fade on the content doesn't move anything and can stay. That's the general rule: with reduced motion, opacity and color stay, movement goes.
In Tailwind
Tailwind has h-auto and variants for the state (data-open:, open:) and for the pseudo-element (details-content:). There are no classes for interpolate-size or calc-size(): they go in brackets.
| CSS | Tailwind |
|---|---|
| interpolate-size: allow-keywords | [interpolate-size:allow-keywords] |
| height: 72px → auto | h-18 data-open:h-auto |
| height: calc-size(auto, size) | h-[calc-size(auto,size)] |
| transition: height 350ms | transition-[height] duration-350 |
| ::details-content { block-size: 0 } | details-content:block-0 |
| transition-behavior: allow-discrete | transition-discrete |
| grid-template-rows: 0fr → 1fr | grid-rows-[0fr] data-open:grid-rows-[1fr] |
| no transition with reduced motion | motion-reduce:transition-none |
<html class="[interpolate-size:allow-keywords]">…<details name="spec" class="details-content:block-0details-content:overflow-y-clipdetails-content:transition-[block-size,content-visibility]details-content:transition-discretedetails-content:duration-350open:details-content:block-autoopen:details-content:duration-400motion-reduce:details-content:transition-none"><summary>Dimensions</summary><dl>…</dl></details></html>