Skip to content

Animate Elements To and From Top Layer

Elements that render in the “top layer” (like <dialog>, elements with the popover attribute, or tooltips) have historically been difficult to animate because they toggle between display: none and a visible state. Modern CSS provides @starting-style, transition-behavior: allow-discrete, and the overlay property to enable smooth entry and exit transitions for these elements. Note that native CSS nesting is used in the examples below.

To animate the display property, you must set transition-behavior: allow-discrete. This allows the element to remain visible during its exit transition. If using the transition shorthand, place transition-behavior: allow-discrete afterward so the shorthand does not reset it.

When an element moves in or out of the top layer, it must transition the overlay property. This ensures the element stays in the top layer for the duration of the animation, preventing it from being clipped by other elements or the viewport prematurely.

Use the @starting-style at-rule to define the styles an element should transition from when it is first rendered or its display changes from none.

The ::backdrop pseudo-element can be animated similarly by applying transitions to its own properties.

/* 1. Define the visible (open) state */
dialog[open],
[popover]:popover-open {
opacity: 1;
/* 2. Define the starting state for entry (must come after open state) */
@starting-style {
opacity: 0;
}
/* Scale only for users who haven't asked for reduced motion (see 5) */
@media (prefers-reduced-motion: no-preference) {
transform: scale(1);
@starting-style {
transform: scale(0.9);
}
}
}
/* 3. Define the base (closed/exit) state and transitions */
dialog,
[popover] {
opacity: 0;
/* MANDATORY: transition display and overlay for top-layer elements */
transition-property: opacity, transform, display, overlay;
transition-duration: 0.3s;
transition-timing-function: ease-out;
/* Separate after transition so a shorthand doesn't reset it or fail in older browsers */
transition-behavior: allow-discrete;
@media (prefers-reduced-motion: no-preference) {
transform: scale(0.9);
}
}
/* 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;
/* Separate after transition so a shorthand doesn't reset it or fail in older browsers */
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: the scale above only applies
with no-preference, so this only shortens the fade */
@media (prefers-reduced-motion: reduce) {
dialog,
[popover] {
transition-duration: 0.1s;
}
}
  • MANDATORY: Include overlay in your transition list for any element moving into or out of the top layer.
  • MANDATORY: Use transition-behavior: allow-discrete when transitioning display and overlay.
  • DO NOT: Use allow-discrete in the transition shorthand — it will make older browsers ignore the entire transition declaration. Except in use cases where that is desirable, use a separate transition-behavior: allow-discrete declaration.
  • MANDATORY: Respect user preferences for reduced motion by applying transforms only inside @media (prefers-reduced-motion: no-preference) and shortening the transition duration inside @media (prefers-reduced-motion: reduce), so users who prefer reduced motion get a short fade.
  • DO: Place the @starting-style block inside or after the “open” state selector to ensure proper cascading.
  • DO NOT: Use @starting-style for exit animations; exit animations are defined by the transition to the base (closed) state.

Baseline: starting-style Baseline: overlay

Baseline: transition-behavior

Entry animations work in pure CSS across all browsers that support @starting-style—no .is-opening class is needed because entry transitions do not depend on overlay or discrete display transitions.

Exit animations require both overlay and discrete display transition support. When either is unsupported (such as in Firefox and Safari), wrap the open-state selectors in :is() and append :where(:not([data-closing])) (nesting &::backdrop and @starting-style) so setting data-closing triggers the exit transition while the element remains in the top layer, then wait for getAnimations() to settle before calling .close() or .hidePopover():

:is(dialog[open], [popover]:popover-open):where(:not([data-closing])) {
opacity: 1;
@starting-style {
opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
transform: scale(1);
@starting-style {
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);
});

Popover light dismiss and popovertarget toggles cannot be intercepted (beforetoggle is only cancelable when opening), so in browsers that need the fallback those exits are instant. Provide an explicit close control that calls closeTopLayer() if the exit animation matters.

Baseline: popover

Compatibility notes: Badges like Baseline: … mark web features referenced by this guide. Baseline status describes availability across the core browser set — it does not mean every browser or older version supports the feature. Feature data as published upstream, synced 2026-11-12.

Open the demo standalone · Demo source on GitHub

Counter-example (negative demo) — what this guide tells you to avoid.


Source: GoogleChrome/modern-web-guidance-src (Apache-2.0), upstream commit 179dbc6, synced 2026-11-12. Local changes: macros expanded to links/badges, first heading de-duplicated, demo and this footer added. This is an independent, unofficial site — not a Google publication.