Back to notes

@starting-style: animate popovers in and out

6 November 2026·
cssanimationpopoverbaseline
Live demoOpen full screen

A popover that slides in instead of just appearing takes three CSS rules and zero JavaScript, and that part works in Chrome, Firefox and Safari. Making it slide out again is a different story, and while preparing this note I found out it's messier than the "Baseline" label suggests. In the demo above, open the popover and the dialog, add and remove cards, and turn @starting-style off to see the difference. The chips at the top tell you what your browser can do.

The problem it solves

MDN puts it clearly: CSS transitions are not triggered on an element's first style update, or when its display changes from none to something else. That's on purpose, but it means a popover, a <dialog> or a node you've just appended has no "before" value to transition from. It simply appears.

@starting-style gives it one. It defines the values an element transitions from when it receives its first style update: when it's inserted into the DOM or goes from display: none to visible. Exits are the other half, and they need transition-behavior: allow-discrete.

How it works

This is the core of the popover in the demo:

/* 1. Where it ends up when open */
.pop:popover-open { opacity: 1; transform: none; }

/* 2. Closed state: where the exit goes */
.pop {
  opacity: 0;
  transform: translateY(28px) scale(.9);
  transition:
    opacity .5s, transform .5s,
    overlay .5s allow-discrete,
    display .5s allow-discrete;
}

/* 3. Where it comes from on the first frame */
@starting-style {
  .pop:popover-open { opacity: 0; transform: translateY(28px) scale(.9); }
}

There are three states, and that's the key idea. The closed state, the open state and the starting state. MDN shows they don't have to be the same: an element enters from its @starting-style values and leaves towards its default values. In the demo they match, so it comes and goes the same way.

The pieces:

  • @starting-style only applies on that first frame. After that it's ignored, which is why closing doesn't go "back" to it.
  • Order matters. According to MDN, the @starting-style rule and the original rule have the same specificity, so put @starting-style after it or the original wins. You can also nest it inside the rule, as I do with the cards: .item { @starting-style { opacity: 0; } }.
  • Pseudo-elements need a standalone block. Nesting doesn't work for them, so the dialog's backdrop uses @starting-style { dialog[open]::backdrop { ... } } on its own.
  • display ... allow-discrete is what makes the exit visible. display is a discrete property: by default it doesn't transition at all. With allow-discrete, MDN explains that going from none to block flips at 0% of the duration and going to none flips at 100%, so the element stays visible for the whole animation. The initial value of transition-behavior is normal.
  • overlay is for elements in the top layer (open popovers, modal dialogs). Authors can't set it; the browser does. Adding it to the transition list defers the removal from the top layer until the animation ends.
  • Only for transitions. If you use @keyframes animations for the entry, you don't need @starting-style.

The cards panel uses the same rule for nodes created with appendChild(). Removing them is the classic pattern: add a class that animates to the "from" state and call remove() when it ends.

The log panel lists the transitionrun events the browser fires. In Chromium, closing the popover shows display and overlay in that list. Elsewhere you'll see a different story.

Browser support and gotchas

MDN marks @starting-style as Baseline 2024, available since August 2024. Its compatibility data lists Chrome 117, Firefox 129 and Safari 17.5. The entry animation is the safe part.

The exit is not:

  • Firefox doesn't transition display yet. MDN's compatibility data marks it as unsupported in Firefox, and its transition-behavior support as partial for that reason. The work landed in Mozilla's code this summer, but behind a preference, layout.css.display-animations.enabled, which is still false by default in the source. On October 13, 2026, a Mozilla engineer noted in the tracking bug that this would move the transition-behavior feature from "Newly Available" back to "Limited Availability". The MDN page for transition-behavior already shows "Limited availability".
  • Safari transitions display, but not out of the top layer. MDN lists Safari 18 for display transitions, and overlay as Chromium-only (and experimental). In the Playwright WebKit build I test the demo with, a plain div fades out fine, while the popover and the dialog vanish the moment they close. WebKit's bug to implement overlay and the new top-layer exit algorithm is still open.
  • You can't detect it with @supports. CSS.supports('transition-behavior', 'allow-discrete') returns true in all three browsers I tested with, even where the exit doesn't work. The demo runs a small runtime check, adapted from one posted in Mozilla's bug:
function supportsDisplayTransition() {
  const div = document.createElement('div');
  div.style.transition = 'display 1s allow-discrete';
  document.body.append(div);
  const cs = getComputedStyle(div);
  cs.display; // flush styles
  div.style.display = 'none';
  const ok = cs.display !== 'none'; // still visible = it's transitioning
  div.remove();
  return ok;
}
const nativeExit = supportsDisplayTransition() && CSS.supports('overlay', 'auto');

If nativeExit is false, the demo's Close buttons, and Esc in the dialog, animate the element with Element.animate() while it's still open and only then call hidePopover() or close(). Light-dismissing the popover (clicking outside) stays instant in those browsers; there's no way to delay it. My first plan B used a CSS class instead of Element.animate(), and in WebKit the transition sometimes didn't start when triggered by Esc. The Web Animations version was reliable in all three.

  • Motion. With prefers-reduced-motion, the demo keeps the fades and drops the transforms and blur.

What to keep an eye on

  • Firefox flipping the preference. Once layout.css.display-animations.enabled is on by default, exits will work there with the same CSS.
  • overlay in WebKit. Until then, Safari will keep cutting top-layer exits short.
  • The Baseline status of transition-behavior. If it drops to "Limited", tools that rely on web-features data will start flagging it. Entry animations with @starting-style are unaffected.

Sources