max-height y su defecto de temporización
Por qué la técnica clásica produce tiempos muertos, el cálculo exacto del error, las dos mitigaciones que funcionan y cuándo sigue siendo defendible.
Durante veinte años, la respuesta a “cómo animo un acordeón” fue animar max-height de cero a un valor grande. Funciona en cualquier navegador y en cualquier época, y tiene un defecto que la mayoría de quienes la usan han visto sin diagnosticar: la animación tarda lo que dice el CSS, pero el movimiento visible dura otra cosa. Esta lección pone el número a ese defecto, porque saber cuánto vale decide si la técnica es aceptable en tu caso o no.
- Calcular exactamente cuánto tiempo muerto introduce un valor de
max-heightdado. - Explicar por qué el defecto es asimétrico entre abrir y cerrar.
- Aplicar las dos mitigaciones que reducen el error sin cambiar de técnica.
- Decidir con criterio cuándo
max-heightsigue siendo la elección correcta.
La técnica
.cuerpo {
max-height: 0;
overflow: hidden;
transition: max-height 300ms linear;
}
.desplegable[data-abierto] .cuerpo {
max-height: 1000px; /* una apuesta */
}
max-height acepta longitudes, así que interpola sin problema. Y como es un máximo y no una altura, cuando su valor supera la altura real del contenido, el elemento simplemente mide lo que mide y no se estira. Esa es la razón por la que la técnica funciona: el valor grande no infla el elemento, solo deja de limitarlo.
El cálculo del tiempo muerto
Supón que la altura real del contenido es de 200 píxeles y el máximo declarado es de 1000. La transición interpola max-height de 0 a 1000 en 300 milisegundos con curva lineal.
El elemento crece mientras max-height sea menor que 200. A partir de ahí, el máximo ya no limita nada y el elemento se queda quieto en su altura real, aunque la propiedad siga subiendo hasta 1000.
max-height alcanza 200 cuando el progreso vale 200 dividido entre 1000, es decir 0.2. Con curva lineal, eso ocurre a los 60 milisegundos.
El movimiento visible dura 60 milisegundos de los 300 declarados. Los otros 240 no se ve nada: el elemento ya está abierto y la propiedad sigue subiendo en silencio. Es decir, la animación es cinco veces más rápida de lo que pediste y luego hay un cuarto de segundo de nada.
Al cerrar, el defecto se invierte. max-height baja de 1000 a 0. El elemento no cambia de altura hasta que el máximo baja por debajo de 200, lo que ocurre a los 240 milisegundos. Hay 240 milisegundos de inmovilidad y después 60 de colapso.
| Dirección | Tiempo muerto | Movimiento visible |
|---|---|---|
| Abrir | 0 ms al principio | 60 ms |
| Abrir | 240 ms al final | — |
| Cerrar | 240 ms al principio | — |
| Cerrar | 0 ms al final | 60 ms |
La fórmula general es directa. Con altura real h y máximo declarado M, la fracción útil de la animación es h / M, y el resto es tiempo muerto. Con h de 200 y M de 1000, la fracción útil es el 20 por ciento.
// Cuanto de tu animacion se ve realmente.
function fraccionUtil(alturaReal, maximoDeclarado) {
return Math.min(alturaReal / maximoDeclarado, 1);
}
console.log(fraccionUtil(200, 1000)); // 0.2 -> el 20% de la duracion
console.log(fraccionUtil(900, 1000)); // 0.9 -> el 90%, casi correcto
console.log(fraccionUtil(200, 250)); // 0.8 -> el 80%
De la fórmula sale la contradicción que define esta técnica: para que el tiempo muerto sea pequeño, el máximo tiene que estar cerca de la altura real; pero si estuviera cerca de la altura real, no necesitarías el truco. La apuesta tiene que ser holgada para no romperse con contenido grande, y cuanto más holgada, peor la temporización.
Y si la apuesta se queda corta, el fallo no es de temporización sino funcional: el contenido se recorta y el usuario no puede leerlo. Esa asimetría —fallar por exceso es feo, fallar por defecto es grave— empuja a todo el mundo a poner valores enormes, que es justo lo que produce el peor tiempo muerto.
El defecto de temporización se suele describir como un problema estético —“se abre demasiado rápido”— y esa descripción se queda corta. El daño real aparece cuando la animación de altura no está sola, que es prácticamente siempre. Si acompañas el desplegable con un desvanecido del contenido de 300 milisegundos, la altura termina a los 60 y la opacidad a los 300: el contenido aparece dentro de un hueco que ya está abierto, durante un cuarto de segundo, y la relación entre las dos animaciones se pierde. Si además rotas una flecha para indicar el estado, la flecha gira durante 300 milisegundos mientras el panel lleva 240 abierto, y el usuario ve la señal de estado desincronizada del estado. Y el caso peor, que es también el más frecuente: el escalonado de varios desplegables se destruye por completo, porque cada uno tiene una altura distinta y por tanto una fracción útil distinta, así que uno de 100 píxeles se abre en 30 milisegundos y uno de 800 en 240, con el mismo CSS y la misma duración. Lo que era un escalonado calibrado se convierte en un desorden que depende del contenido. La conclusión es que el problema de max-height no se arregla ajustando su duración, porque la duración efectiva no es una constante: es una función del contenido, y por tanto ninguna otra animación puede sincronizarse con ella. Cualquier técnica cuya duración real dependa de datos es incompatible con un sistema de movimiento coherente, y esa es la razón de fondo por la que hay que sustituirla, mucho más que el tiempo muerto en sí.
Las dos mitigaciones
Mitigación uno: ajustar el máximo a un valor plausible por componente. Si sabes que un desplegable concreto nunca pasará de 300 píxeles, pon 320 en lugar de 1000. La fracción útil sube del 20 al 94 por ciento y el defecto casi desaparece. El precio es que la apuesta es más frágil, así que solo vale para contenido acotado por diseño.
Se puede parametrizar por componente sin duplicar la regla:
.cuerpo {
--max: 1000px;
max-height: 0;
overflow: hidden;
transition: max-height 300ms linear;
}
.desplegable[data-abierto] .cuerpo { max-height: var(--max); }
.desplegable.compacto .cuerpo { --max: 220px; }
.desplegable.grande .cuerpo { --max: 900px; }
Mitigación dos: usar curvas asimétricas que concentren el recorrido donde importa. Como el tramo útil está al principio al abrir y al final al cerrar, una curva que avance despacio al principio hace que el tramo útil dure más.
.cuerpo {
max-height: 0;
overflow: hidden;
/* Al cerrar: baja rapido al principio, asi llega antes a la zona util. */
transition: max-height 260ms cubic-bezier(0.6, 0, 1, 1);
}
.desplegable[data-abierto] .cuerpo {
max-height: var(--max);
/* Al abrir: sube despacio al principio, la zona util dura mas. */
transition: max-height 260ms cubic-bezier(0.2, 0, 0.7, 1);
}
Las dos mitigaciones juntas reducen mucho el problema y no lo eliminan, porque la fracción útil sigue dependiendo del contenido. Sirven para hacer soportable código existente, no para justificar escribir código nuevo así.
Cuándo sigue siendo defendible
Hay tres situaciones donde max-height es una elección razonable en 2026.
Cuando el contenido está acotado por diseño y conoces el rango. Un panel de notificaciones que nunca muestra más de tres elementos de altura fija. La apuesta es segura y ajustada, la fracción útil es alta y no hay defecto perceptible.
Cuando no puedes añadir el envoltorio que exige la técnica de rejilla. En sistemas donde el marcado lo genera algo que no controlas, meter un div extra puede no ser posible. max-height no exige estructura.
Cuando la duración es tan corta que el tiempo muerto no se percibe. Con una transición de 120 milisegundos y una fracción útil del 30 por ciento, el movimiento dura 36 milisegundos y el tiempo muerto 84. A esa escala, la incoherencia está por debajo del umbral perceptivo para la mayoría de la gente.
Fuera de esos tres casos, la técnica de rejilla hace lo mismo sin apostar ningún número, y la medición desde JavaScript lo hace con precisión exacta cuando hace falta control fino.
- Monta un desplegable con
max-height: 1000pxy contenido de unos 200 píxeles. Cronometra con una grabación cuánto dura el movimiento visible. - Comprueba que el número coincide con el que predice la fórmula de la fracción útil.
- Añade un desvanecido del contenido de la misma duración y observa la desincronización.
- Aplica las dos mitigaciones y vuelve a cronometrar. Anota cuánto has recuperado y cuánto queda.
- Duplica la altura del contenido sin tocar nada y comprueba cómo cambia la duración efectiva.