← Back to gallery
CSS

Top Layer Discrete Entry Exit

Native dialog and popover surfaces can animate into and out of the top layer by pairing @starting-style with discrete display and overlay transitions, so panels and backdrops stay visible until their exit fade or scale completes.

top-layerdialogpopoverstarting-styletransition-behaviorallow-discreteoverlayprefers-reduced-motion

Modal transition / Top layer

Top Layer Discrete Entry Exit

A native dialog or popover can fade and scale in, then stay in the top layer long enough to finish its exit motion by pairing @starting-style with discrete display and overlay transitions.

Native dialog · display + overlay

Dialog Exit Retention

A modal panel keeps display and overlay discrete transitions aligned with the fade/scale exit.

  • display
  • overlay
  • allow-discrete

Popover · @starting-style

Popover Entry Frame

A popover receives a real entry frame before it settles into the browser-managed top layer.

  • popover
  • @starting-style
  • entry

Dialog backdrop · synchronized exit

Backdrop Pairing

Backdrop opacity, panel opacity, display, and overlay all leave on the same clock.

  • ::backdrop
  • paired exit
  • top layer

Top layer inspector

Dialog Exit Retention

  • display
  • overlay
  • allow-discrete

The panel fades out while display and overlay wait for the transition to finish. The element does not vanish from the top layer on the first closing frame.

Helped you ship something? 🐟 Send my cat a churu

.top-layer-card {
  opacity: 0;
  transform: translateY(18px) scale(.96);
  transition:
    opacity 0.28s ease,
    transform 0.28s cubic-bezier(.2, .8, .2, 1),
    overlay 0.28s allow-discrete,
    display 0.28s allow-discrete;
}
.top-layer-card[open] {
  opacity: 1;
  transform: translateY(0) scale(1);
}
@starting-style {
  .top-layer-card[open] {
    opacity: 0;
    transform: translateY(18px) scale(.96);
  }
}
.top-layer-card::backdrop {
  opacity: 0;
  background: rgb(2 6 23 / .56);
  transition:
    opacity 0.28s ease,
    overlay 0.28s allow-discrete;
}
.top-layer-card[open]::backdrop {
  opacity: 1;
}
@starting-style {
  .top-layer-card[open]::backdrop { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .top-layer-card {
    transition: none;
    transform: none;
  }
}

How to make this

A top-layer discrete entry/exit transition keeps native dialog and popover surfaces visible long enough for their close animation by transitioning display and overlay with allow-discrete.

html
<button data-open-dialog>  Open billing</button> 5<dialog class="top-layer-card" id="billing-dialog" aria-labelledby="billing-title">  <form method="dialog" class="top-layer-card__panel">    <p>Plan change</p>    <h2 id="billing-title">Confirm upgrade</h2>    <button value="cancel">Cancel</button>  </form></dialog> <style>.top-layer-card {  border: 0;  padding: 0;  background: transparent;  color: #e2e8f0;  opacity: 0;  transform: translateY(16px) scale(.96);  transition:    opacity .24s ease,    transform .24s cubic-bezier(.22, 1, .36, 1),24    overlay .24s allow-discrete,    display .24s allow-discrete;26  transition-behavior: allow-discrete;}.top-layer-card[open] {  opacity: 1;  transform: translateY(0) scale(1);}.top-layer-card::backdrop {  background: rgba(2, 6, 23, .58);  opacity: 0;  transition:    opacity .24s ease,    overlay .24s allow-discrete,    display .24s allow-discrete;  transition-behavior: allow-discrete;}.top-layer-card[open]::backdrop { opacity: 1; }42@starting-style {  .top-layer-card[open] {    opacity: 0;    transform: translateY(16px) scale(.96);  }  .top-layer-card[open]::backdrop { opacity: 0; }}.top-layer-card__panel {  width: min(22rem, calc(100vw - 2rem));  padding: 1rem;  border-radius: 18px;  background: #0f172a;  border: 1px solid rgba(148, 163, 184, .22);  box-shadow: 0 24px 50px rgba(2, 6, 23, .46);}.top-layer-card__panel button { margin-top: 1rem; }58@media (prefers-reduced-motion: reduce) {  .top-layer-card,  .top-layer-card::backdrop {    transition: none;  }}</style> <script>const dialog = document.querySelector('#billing-dialog');const trigger = document.querySelector('[data-open-dialog]');69trigger.addEventListener('click', () => dialog.showModal());</script>

Annotated snippet

  1. Line 5The dialog element enters the browser top layer when showModal() runs. This is different from a high z-index panel because it escapes local stacking contexts and pairs with a native backdrop.
    PitfallWhy not just animate a fixed element with z-index?

    The top layer is above normal stacking contexts, including transformed parents and sticky headers. Dialog and popover also bring native behavior such as backdrops, Escape handling, focus behavior, and popover dismissal rules that a plain z-index panel must recreate.

  2. Line 24overlay and display are discrete properties. Including them with allow-discrete tells the browser to delay the top-layer and display flips until the opacity and transform exit have time to finish.

    Both cards close on the same rhythm. The before card removes the surface at the start of close, so it snaps away. The after card keeps the panel layered while opacity finishes, which is what display and overlay allow-discrete protects.

    PitfallWhy do display and overlay need allow-discrete?

    display and overlay do not interpolate like opacity. allow-discrete lets the browser schedule the value flip at the correct edge of the transition, so the closing surface can remain rendered in the top layer until the visual exit completes.

  3. Line 26transition-behavior is written explicitly even though the shorthand also says allow-discrete. That keeps the intent readable and protects teams that later split the transition declaration.
    PitfallWhy do display and overlay need allow-discrete?

    display and overlay do not interpolate like opacity. allow-discrete lets the browser schedule the value flip at the correct edge of the transition, so the closing surface can remain rendered in the top layer until the visual exit completes.

  4. Line 42@starting-style supplies the first open frame. Without it, a dialog or popover can appear already-open, leaving opacity and transform with no previous value to interpolate from.

    A top-layer element that becomes open immediately needs a declared first frame. Without @starting-style it snaps in; with it, the same panel has a real entry path.

    PitfallWhy does entry need @starting-style?

    A dialog or popover often appears in its final open state as soon as it enters the top layer. @starting-style defines the pre-open opacity and transform for that first rendered frame, giving the transition a real starting value.

  5. Line 58Reduced motion removes travel and fades, not the native modal behavior. The close button, Escape behavior, backdrop, and focus return should stay exactly the same.
    PitfallWhat should reduced motion do here?

    Disable the fade and scale travel while preserving the same open and closed states. Reduced motion should not bypass native dialog semantics or leave the backdrop and focus behavior different from the animated path.

  6. Line 69JavaScript only opens the native dialog. The animation remains CSS-owned, so the same pattern can be reused for popover with :popover-open and element.showPopover().
    PitfallDoes this require JavaScript animation code?

    No. Use JavaScript to call showModal(), close(), showPopover(), or hidePopover() when interaction requires it. Keep the timing, easing, display retention, and backdrop fade in CSS.

Other pitfalls

What is the fallback when allow-discrete is unsupported?
Keep the native dialog or popover behavior and let the entry or exit snap. The fallback should still open, close, restore focus, and expose the same content. Feature queries can layer the discrete transition only where transition-behavior, @starting-style, and overlay/display transitions are supported.
Who owns focus and close behavior during the exit animation?
The native primitive owns interaction state. CSS can retain the visual surface for exit, but JavaScript should not fake a second closing state that traps focus after the dialog has closed. Store the trigger, let close or hidePopover finish the semantic state change, and verify focus returns predictably.

Notes

Overview

Top-layer entry and exit transitions are about lifecycle, not decoration. A native <dialog> or popover can appear above every normal stacking context, but closing it changes discrete state: display and overlay do not interpolate like opacity. This pattern lets the browser keep the surface in the top layer long enough for the visual exit to finish, while the native primitive still owns focus, backdrop, and dismissal behavior.

When to use it

Use it for modal dialogs, popovers, command palettes, contextual menus, and teaching demos where the surface needs a real open and close motion but should not be rebuilt as a custom z-index stack. Skip it when the panel is purely decorative or when the browser support target cannot tolerate a snapped fallback. A clean snap is better than a fake JavaScript modal that loses native close and focus rules.

How it works

The open state transitions normal animatable properties such as opacity and transform. @starting-style supplies the first open frame so the browser has a value to animate from when the element first enters the top layer. On close, transition-behavior: allow-discrete plus display and overlay in the transition list tells the browser to delay those discrete flips until the visual exit has played. The backdrop follows the same timing so the page does not brighten before the surface finishes leaving.

Production gotchas

Browser support is the first production decision. If @starting-style, transition-behavior, or overlay retention is missing, the UI should still open and close natively; only the polish snaps. Keep that fallback behind @supports instead of writing a second JavaScript animation state. Also avoid animating layout around the surface: compare demos need fixed-height wrappers, and production dialogs should not push page content while they leave.

Accessibility

The native element owns interaction. Open dialogs with showModal(), close them with close() or method="dialog", and use popover APIs for non-modal surfaces. Store the trigger when you need explicit focus return, but do not trap focus in a fake closing shell after the native primitive has closed. Under prefers-reduced-motion: reduce, remove travel and fades while preserving the same close button, Escape behavior, backdrop state, and focus path.

References

Implementation depth

This pattern is about top-layer lifecycle, not modal layout. @starting-style gives dialog and popover a first open frame, while display and overlay allow-discrete keep the surface rendered until the exit transition completes.

Keep the native primitive in charge. JavaScript should call showModal, close, showPopover, or hidePopover; CSS should own the entry, exit, backdrop, reduced-motion fallback, and the no-layout-shift compare behavior.

The fallback is semantic continuity, not motion parity. If a browser cannot retain display or overlay through the transition, the surface may snap, but the modal or popover must still close through the native API and return focus through the same ownership path.