12k
All articles

Cómo animar display: none sin hacks de JavaScript

Anima display none con CSS usando allow-discrete y @starting-style, además de overlay para dialog y popover y notas de compatibilidad.

OpenReplay Team
OpenReplay Team
Cómo animar display: none sin hacks de JavaScript

Para animar un elemento hacia o desde display: none usando solo CSS, añade display a tu transition con la palabra clave allow-discrete y proporciona el estado inicial en una regla @starting-style — sin JavaScript, sin setTimeout, sin listeners de transitionend. Dos funcionalidades fueron implementadas para hacer esto posible: transition-behavior: allow-discrete permite que propiedades discretas como display participen en una transición, y @starting-style le proporciona al navegador un estado “antes de abrirse” desde el cual animar la entrada. Esta guía incluye recetas de salida y entrada listas para copiar y pegar, el caso de diálogos y popovers en la capa superior con su problema particular de overlay, y una evaluación honesta del soporte actual en los distintos navegadores.

Puntos clave

  • Anima display incluyéndolo en la propiedad abreviada transition con allow-discrete, y define el estado de entrada en un bloque @starting-style — sin JavaScript.
  • display es una propiedad discreta: con allow-discrete, el navegador la cambia a visible en el 0% durante la entrada y a none en el 100% durante la salida, de modo que el contenido permanece visible durante toda la duración.
  • Si usas la propiedad independiente transition-behavior: allow-discrete, debe ir después de la propiedad abreviada transition, de lo contrario el navegador la ignorará.
  • Para diálogos y popovers, también aplica la transición overlay ... allow-discrete, pero trátalo como mejora progresiva, ya que overlay aún no forma parte de Baseline.
  • @starting-style y allow-discrete forman parte de Baseline como funcionalidades recién disponibles desde que Firefox 129 fue lanzado el 6 de agosto de 2024; en navegadores sin soporte, el elemento simplemente aparece y desaparece de forma instantánea.

Por qué display: none no podía ser animado

Una transición CSS necesita un estilo previo al cambio desde el cual animar. Un elemento con display: none — o uno que acaba de ser insertado en el DOM — no tiene un estado renderizado previo, por lo que históricamente el navegador no tenía nada sobre lo cual interpolar y la transición nunca se ejecutaba. Por eso los desarrolladores recurrían a hacks de alternancia de clases con setTimeout o doble requestAnimationFrame para forzar un reflujo.

El atajo habitual — desvanecer con opacity: 0 y dejar el elemento en el DOM — no es un sustituto válido. opacity: 0 deja el elemento en el flujo de diseño, en el orden de tabulación y en el árbol de accesibilidad, por lo que los usuarios de teclado y lectores de pantalla aún pueden acceder a contenido que parece haber desaparecido. display: none lo elimina de los tres. En la práctica, las reproducciones de sesión en componentes migrados a medias revelan exactamente esta clase de error: un usuario que tabula o hace clic en un panel que parece cerrado pero que solo fue desvanecido y nunca fue eliminado del flujo de diseño. Animar con display: none real elimina este problema.

Las dos funcionalidades CSS que lo resuelven: allow-discrete y @starting-style

display es una propiedad discreta — no puede interpolar entre valores, es un interruptor de encendido/apagado. Las propiedades animadas de forma discreta generalmente cambian entre dos valores en el 50% de la animación; la excepción ocurre cuando se anima hacia o desde display: none o content-visibility: hidden, en cuyo caso el navegador realiza el cambio de manera que el contenido animado sea visible durante toda la duración de la animación. La dirección importa: al animar display de none a block, el valor cambia a block en el 0% de la duración para que sea visible desde el inicio; al animar de block a none, cambia a none en el 100% de la duración para que sea visible hasta el final — lo cual es lo que mantiene el desvanecimiento realmente visible en ambas direcciones.

Para habilitarlo, establece transition-behavior: allow-discrete en la transición de display, y proporciona el estado de entrada con @starting-style.

La receta de salida: transición hacia display: none

Para desvanecer un elemento y luego eliminarlo del flujo de diseño, incluye display en la transition con allow-discrete y define el estado oculto:

.panel {
  opacity: 1;
  transition: opacity 0.3s ease, display 0.3s allow-discrete;
}

.panel.is-hidden {
  opacity: 0;
  display: none;
}

La opacidad se anima hasta 0 en 300ms; display se mantiene en su valor visible hasta el 100% y luego cambia a none. Si omites allow-discrete en display, el elemento desaparece de forma instantánea — el error más común con esta técnica.

La receta de entrada: animar desde display: none

Para un elemento que comienza oculto, coloca los valores “antes de abrirse” en una regla @starting-style. Con anidamiento CSS, todo queda en un solo bloque:

.panel {
  display: none;
  opacity: 0;
  transition: opacity 0.3s ease, display 0.3s allow-discrete;
}

.panel.is-open {
  display: block;
  opacity: 1;

  @starting-style {
    opacity: 0;
  }
}

El orden importa. @starting-style tiene la misma especificidad que la regla a la que apunta, por lo que debe ir después de la declaración del estado abierto para ganar en la cascada. Y hay una trampa en una sola línea: si aplicas transition-behavior: allow-discrete antes de la propiedad abreviada transition, el navegador ignorará el transition-behavior. Cuando se escribe de forma independiente, debe ir al final:

.panel {
  transition: opacity 0.3s, display 0.3s;
  transition-behavior: allow-discrete; /* debe ir DESPUÉS de la propiedad abreviada */
}

Para motores más antiguos, el patrón multiplataforma de MDN declara transition dos veces — la primera instancia sin allow-discrete proporciona compatibilidad entre navegadores, garantizando que las demás propiedades sigan siendo animadas en navegadores que no soportan transition-behavior.

Diálogos y popovers: el problema con overlay

Los elementos de la capa superior — <dialog> y cualquier elemento que use el atributo popover — son el caso de uso de mayor valor, y tienen un requisito adicional. Para diálogos y popovers también debes aplicar la transición overlay ... allow-discrete, de lo contrario el elemento abandona la capa superior de forma instantánea y la animación de salida nunca se muestra:

dialog {
  translate: 0 100vh;
  transition:
    translate 0.4s ease-out,
    display 0.4s allow-discrete,
    overlay 0.4s allow-discrete;
}

dialog[open] {
  translate: 0 0;

  @starting-style {
    translate: 0 100vh;
  }
}

Para un popover, reemplaza dialog[open] por la pseudoclase :popover-open. La propiedad overlay es lo que difiere la salida de la capa superior: garantiza que la eliminación del elemento de la capa superior se posponga hasta que la animación haya finalizado; en casos más complejos, no hacerlo puede resultar en que el elemento sea eliminado del overlay demasiado pronto, haciendo que la animación no sea fluida ni efectiva.

Una advertencia honesta que otros omiten: overlay no forma parte de Baseline. MDN lo marca como experimental — esta funcionalidad no es Baseline porque no funciona en algunos de los navegadores más utilizados. Trátalo como mejora progresiva; donde no tiene soporte, el diálogo igualmente se abre y se cierra, simplemente sin la salida diferida.

Soporte en navegadores y degradación elegante

@starting-style y transition-behavior: allow-discrete ambos pasaron a ser Baseline recién disponibles con Firefox 129, lanzado el 6 de agosto de 2024. Sin embargo, animar display en sí mismo requiere más que la implementación de estas dos funcionalidades. Funciona en Chrome y Edge 117+ y en Safari 18+ — Safari 17.4 añadió transition-behavior y 17.5 añadió @starting-style, pero la transición de display con ellas solo funciona a partir de Safari 18. Firefox 129+ soporta ambas funcionalidades, pero hasta mediados de 2026 aún no anima la propiedad display, por lo que en Firefox el elemento simplemente aparece y desaparece de forma instantánea. El uso de display en @keyframes ha funcionado desde Chrome 116.

FuncionalidadEstadoAlternativa
transition-behavior: allow-discreteBaseline (ago. 2024)Aparición/desaparición instantánea
@starting-styleBaseline (ago. 2024)Sin animación de entrada
overlayNo es BaselineEl diálogo igualmente se abre/cierra

Esto es mejora progresiva pura. Sin estas funcionalidades, los elementos que animan hacia la capa superior o desde un estilo display: none simplemente aparecerán en la página sin transición, tal como ocurre hoy en día. Sin polyfills, sin alternativa en JavaScript. Delimita la mejora con detección de funcionalidades si deseas un límite explícito:

@supports (transition-behavior: allow-discrete) {
  /* animaciones modernas de entrada/salida */
}

Cuándo usar View Transitions en su lugar

Usa estas transiciones cuando estés alternando la visibilidad de un elemento existente. Recurre a la API de View Transitions cuando estés añadiendo o eliminando nodos del DOM. Las view transitions en el mismo documento pasaron a ser Baseline recién disponibles el 14 de octubre de 2025, tras el lanzamiento de Firefox 144 el mismo día — con soporte en Chrome 111+, Edge 111+, Safari 18+ y Firefox 144+. Envuelve la mutación del DOM en document.startViewTransition() con una alternativa simple para motores sin soporte:

if (document.startViewTransition) {
  document.startViewTransition(() => card.remove());
} else {
  card.remove();
}

La receta moderna elimina la antigua orquestación en JavaScript: incluye display en tu transition con allow-discrete, define el estado de entrada en @starting-style, añade overlay para elementos de la capa superior, y deja que los navegadores sin soporte recurran al cambio instantáneo. Elimina el setTimeout y publica el CSS.

Preguntas frecuentes

¿Por qué mi animación de salida no se ejecuta aunque añadí allow-discrete a la transición de display?

La causa más común es que la declaración independiente transition-behavior: allow-discrete aparece antes de la propiedad abreviada transition, por lo que el navegador la ignora silenciosamente. Cuando se escribe como propiedad separada, transition-behavior debe aparecer después de la propiedad abreviada transition, de lo contrario esta última la sobreescribe. Si incluyes allow-discrete directamente dentro del valor de transition, el orden dentro de la propiedad abreviada no importa y esta trampa no aplica.

¿Sigo necesitando @starting-style si solo quiero desvanecer un elemento hacia display none?

No. @starting-style solo es necesario para animaciones de entrada, donde el elemento pasa de display: none o es insertado recientemente en el DOM y necesita un estado previo al que animar desde. Una salida pura — animar un elemento ya visible hacia display: none — solo requiere incluir display en la transición con allow-discrete y definir el estado oculto. Añade @starting-style únicamente cuando también quieras animar la entrada del elemento.

¿Qué ocurre en navegadores que no soportan transition-behavior o @starting-style?

El elemento simplemente aparece y desaparece de forma instantánea, exactamente como lo haría sin ninguna animación. Esto es mejora progresiva, por lo que no se necesitan polyfills ni alternativas en JavaScript. Para diálogos y popovers, los navegadores que no soportan la propiedad overlay, que aún no es Baseline, igualmente abren y cierran el elemento correctamente; simplemente omiten la salida diferida de la capa superior. Puedes delimitar la mejora explícitamente con una regla @supports (transition-behavior: allow-discrete).

¿Cuándo debería usar la API de View Transitions en lugar de animar display none?

Usa View Transitions cuando estés añadiendo o eliminando nodos del DOM, y usa transiciones de display cuando estés alternando la visibilidad de un elemento que ya existe en el DOM. Las view transitions en el mismo documento pasaron a ser Baseline recién disponibles el 14 de octubre de 2025, con soporte en Chrome 111+, Edge 111+, Safari 18+ y Firefox 144+. Envuelve la mutación del DOM en document.startViewTransition() con una alternativa simple para motores sin soporte.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.