Históricamente, los elementos que se renderizan en la "capa superior" (como <dialog>, los elementos con el atributo popover o los cuadros de información) han sido difíciles de animar porque alternan entre display: none y un estado visible. El CSS moderno proporciona @starting-style, transition-behavior: allow-discrete y la propiedad overlay para habilitar transiciones de entrada y salida fluidas para estos elementos. Ten en cuenta que en los siguientes ejemplos se usa el anidamiento nativo de CSS.
Implementación
1. Habilita las transiciones discretas
Para animar la propiedad display, debes configurar transition-behavior: allow-discrete. Esto permite que el elemento permanezca visible durante su transición de salida. Si usas abreviaturas de transición, asegúrate de colocar transition-behavior: allow-discrete después para evitar que la abreviatura lo anule.
2. La propiedad overlay
Cuando un elemento entra o sale de la capa superior, debe realizar la transición de la propiedad overlay. Esto garantiza que el elemento permanezca en la capa superior durante la animación, lo que evita que otros elementos o el viewport lo recorten de forma prematura.
3. Animaciones de entrada con @starting-style
Usa la regla-at @starting-style para definir los estilos de los que debe realizar la transición desde un elemento cuando se renderiza por primera vez o cuando su display cambia de none.
4. Cómo animar el fondo
El pseudoelemento ::backdrop se puede animar de manera similar aplicando transiciones a sus propias propiedades.
Ejemplo
/* 1. Define the visible (open) state */
dialog[open],
[popover]:popover-open {
opacity: 1;
transform: scale(1);
/* 2. Define the starting state for entry (must come after open state) */
@starting-style {
opacity: 0;
transform: scale(0.9);
}
}
/* 3. Define the base (closed/exit) state and transitions */
dialog,
[popover] {
opacity: 0;
transform: scale(0.9);
/* MANDATORY: transition display and overlay for top-layer elements */
transition-property: opacity, transform, display, overlay;
transition-duration: 0.3s;
transition-timing-function: ease-out;
/* Applies to discrete properties like display and overlay */
transition-behavior: allow-discrete; /* Note: be sure to write this after the shorthand */
}
/* 4. Animate the backdrop */
dialog::backdrop,
[popover]::backdrop {
background-color: rgba(0, 0, 0, 0);
transition-property: background-color, display, overlay;
transition-duration: 0.3s;
transition-timing-function: ease-out;
transition-behavior: allow-discrete;
}
dialog[open]::backdrop,
[popover]:popover-open::backdrop {
background-color: rgba(0, 0, 0, 0.5);
@starting-style {
background-color: rgba(0, 0, 0, 0);
}
}
/* 5. Respect user preference for reduced motion */
@media (prefers-reduced-motion: reduce) {
dialog,
[popover] {
/* Disable movement and shorten duration for a simple fade */
transform: none;
transition-duration: 0.1s;
}
@starting-style {
dialog[open],
[popover]:popover-open {
transform: none;
}
}
}
Restricciones y accesibilidad
- OBLIGATORIO: Incluye
overlayen tu lista detransitionpara cualquier elemento que se mueva hacia la capa superior o fuera de ella. - OBLIGATORIO: Usa
allow-discretepara la transición de la propiedaddisplay. - OBLIGATORIO: Respeta las preferencias del usuario de reducir el movimiento con
prefers-reduced-motionsimplificando las transiciones (p.ej., quitando las transformaciones y acortando la duración). - SÍ: Coloca el bloque
@starting-styledentro o después del selector de estado "open" para garantizar una cascada adecuada. - NO uses
@starting-stylepara las animaciones de salida, ya que estas se definen por la transición al estado base (cerrado).
Estrategias de resguardo
Resguardos y compatibilidad de navegadores con transition-behavior
Firefox 129 y versiones posteriores analizan transition-behavior: allow-discrete (CSS.supports('transition-behavior', 'allow-discrete') devuelve true) sin realizar la transición de la propiedad display (error de Firefox 1882408), lo que hace que los elementos desaparezcan de inmediato al salir.
Para detectar de forma confiable la compatibilidad con transiciones discretas de display, prueba si el display calculado de un elemento temporal permanece visible cuando se realiza la transición a none, en lugar de depender únicamente de CSS.supports('transition-behavior', 'allow-discrete'):
let supportsDisplayTransition;
function canTransitionDisplay() {
if (supportsDisplayTransition !== undefined) return supportsDisplayTransition;
if (!window.CSS?.supports?.('transition-behavior', 'allow-discrete') || !document.body) {
return false;
}
const probe = document.createElement('div');
// The shorthand is intentional here: browsers that don't parse allow-discrete
// drop the whole declaration, so display: none applies instantly and the probe
// correctly returns false. !important guards against global reduced-motion
// resets like `* { transition: none !important }`.
probe.style.cssText = 'transition: display 1s allow-discrete !important; display: block;';
document.body.appendChild(probe);
getComputedStyle(probe).display;
probe.style.display = 'none';
supportsDisplayTransition = getComputedStyle(probe).display === 'block';
probe.remove();
return supportsDisplayTransition;
}
Resguardo de salida de la capa superior
Las animaciones de entrada funcionan en CSS puro en todos los navegadores que admiten @starting-style. No se necesita la clase .is-opening porque las transiciones de entrada no dependen de las transiciones overlay ni discretas de display.
Las animaciones de salida requieren compatibilidad con overlay y con la transición discreta de display. Cuando no se admite ninguno de los dos (como en Firefox y Safari), une los selectores de estado abierto en :is() y agrega :where(:not([data-closing])) (anidando &::backdrop y @starting-style) para que la configuración de data-closing active la transición de salida mientras el elemento permanece en la capa superior y, luego, espera a que se establezca getAnimations() antes de llamar a .close() o .hidePopover():
:is(dialog[open], [popover]:popover-open):where(:not([data-closing])) {
opacity: 1;
transform: scale(1);
@starting-style {
opacity: 0;
transform: scale(0.9);
}
&::backdrop {
background-color: rgb(0 0 0 / 0.5);
@starting-style {
background-color: transparent;
}
}
}
// Evaluate lazily: canTransitionDisplay() needs document.body, so a top-level
// const would be permanently false if this script runs in <head>.
function supportsTopLayerExit() {
return window.CSS?.supports?.('overlay', 'auto') && canTransitionDisplay();
}
async function closeTopLayer(element) {
if (!supportsTopLayerExit()) {
element.setAttribute('data-closing', '');
const animations = element.getAnimations({ subtree: true });
if (animations.length > 0) {
await Promise.race([
Promise.allSettled(animations.map((a) => a.finished)),
new Promise((r) => setTimeout(r, 2000)),
]);
}
if (!element.hasAttribute('data-closing')) return;
element.removeAttribute('data-closing');
}
element.close();
// Or for popover:
// element.hidePopover();
}
// Route native close requests (Esc, closedby light dismiss) through the same
// helper so they animate too, instead of closing instantly.
dialog.addEventListener('cancel', (event) => {
event.preventDefault();
closeTopLayer(dialog);
});
No se pueden interceptar los cierres ligeros de ventanas emergentes ni los cambios de estado de popovertarget (beforetoggle solo se puede cancelar cuando se abre), por lo que, en los navegadores que necesitan el resguardo, esas salidas son instantáneas. Proporciona un control de cierre explícito que llame a closeTopLayer() si la animación de salida es importante.
Resguardos y compatibilidad de navegadores para Popover
La API de Popover es, en su mayor parte, una mejora progresiva, pero sus comportamientos definitorios (promoción de la capa superior, cierre rápido y la invocación de popovertarget) no tienen un equivalente solo de CSS. Los navegadores más antiguos necesitan un polyfill o un resguardo manual si prefieres no enviar uno.
Polyfill: Para admitir el atributo popover en navegadores más antiguos, carga @oddbird/popover-polyfill de forma condicional. OBLIGATORIO: Detecta la función verificando la propiedad popover en HTMLElement.prototype y carga el polyfill solo cuando no haya compatibilidad nativa. NO lo cargues de forma incondicional.
Con un empaquetador o un mapa de importación:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Sin un empaquetador, importa desde una CDN dentro de un <script type="module">:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
Advertencia de diseño: El polyfill no puede definir la pseudoclase :popover-open real, por lo que aplica una clase .\:popover-open en su lugar. OBLIGATORIO: Combina las dos con :is() o :where(); de lo contrario, los navegadores que no tengan :popover-open descartarán toda la regla:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
Como alternativa, para un resguardo heredado sin un polyfill, usa position: fixed y calcula manualmente las coordenadas a través de getBoundingClientRect() o confía en el posicionamiento predeterminado con inset: auto si eso es aceptable para el caso de uso.