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.
Implementation
Section titled “Implementation”1. Enable Discrete Transitions
Section titled “1. Enable Discrete Transitions”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.
2. The overlay Property
Section titled “2. The overlay Property”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.
3. Entry Animations with @starting-style
Section titled “3. Entry Animations with @starting-style”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.
4. Animating the Backdrop
Section titled “4. Animating the Backdrop”The ::backdrop pseudo-element can be animated similarly by applying transitions to its own properties.
Example
Section titled “Example”/* 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; }}Constraints & Accessibility
Section titled “Constraints & Accessibility”- MANDATORY: Include
overlayin yourtransitionlist for any element moving into or out of the top layer. - MANDATORY: Use
transition-behavior: allow-discretewhen transitioningdisplayandoverlay. - DO NOT: Use
allow-discretein thetransitionshorthand — it will make older browsers ignore the entiretransitiondeclaration. Except in use cases where that is desirable, use a separatetransition-behavior: allow-discretedeclaration. - 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-styleblock inside or after the “open” state selector to ensure proper cascading. - DO NOT: Use
@starting-stylefor exit animations; exit animations are defined by the transition to the base (closed) state.
Fallback strategies
Section titled “Fallback strategies”Baseline: starting-style Baseline: overlay
Baseline: transition-behavior
Top-layer exit fallback
Section titled “Top-layer exit fallback”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.
Live demo
Section titled “Live demo”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.