Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 10.3Intermediate ~21 min · shot 3 of 6

Enter and Exit Animations

In one line

An element that appears has no "before" style to transition from, and one that is hidden with display: none is gone before it can fade. @starting-style gives the entry a start, and transition-behavior: allow-discrete keeps display (and overlay) on until the exit ends.

0/6 · 0%

Think of it like this

An actor walks on stage and off again. Walking on needs a spot in the wings to start from. Walking off needs the stage lights to stay on until the actor is out of sight. Turning the lights off the instant the exit begins makes the actor vanish. CSS had no wings and no timed lights until recently.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

@starting-style
A rule that gives an element the style to transition from the first time it is rendered.
discrete property
A property whose values are not numbers on a scale (such as display), so it flips instead of blending.
allow-discrete
A transition-behavior value that lets a discrete property take part in a transition and flip at the end of an exit.
top layer
A layer above the page where dialogs and popovers draw, whatever z-index the page uses.
overlay
A property that says whether an element is in the top layer. Transitioning it keeps a closing popover on top until its exit ends.

Step by step

01Show the card

Press Show / hide. The card starts at opacity: 0 with translate: 0 14px (from @starting-style) and eases in over 400 ms. After 200 ms: opacity 0.80 and translate 2.77 px. The console says allow-discrete: true.

02Hide it half way

Pause and step: press Show / hide, wait 200 ms, press it again. Three tracks appear: display (400 ms), opacity (303 ms) and translate (303 ms). At 400 ms the card is at opacity 0.06 and still display: block. At 600 ms it is display: none.

03Remove it from the page

Press Remove from the page: the card is gone at once. No exit ran, because the element no longer exists.

04Exit, then remove

Wait for the animations before remove().

leave.jswhole filejs
async function leave(el) {
  el.classList.add("gone"); // starts the exit transitions
  await Promise.allSettled(el.getAnimations().map((a) => a.finished));
  el.remove();
}

05Compare the two popovers

Open each popover and close it after half a second. The one with display and overlay fades out over 400 ms (opacity 0.24 after 200 ms). The one without is display: none at once.

Try it yourself

  1. 1

    Change the entry

    In the card example change translate: 0 14px in the @starting-style rule to translate: -40px 0. After Run, press Show: the card slides in from the left. Pause at 200 ms: the translate reads about -7.9 px.

  2. 2

    Reverse the exit

    Press Show and wait until it has arrived, press Hide, and press Show again 150 ms later (pause and step to be exact). The new entrance tracks for opacity and translate last 230 ms, not 400, because the browser reverses the half-finished exit and shortens it in proportion.

  3. 3

    Honour reduced motion

    Add @media (prefers-reduced-motion: reduce) { .card { translate: none; } .card.gone { translate: none; } @starting-style { .card { translate: none; } } } and switch on Reduced motion in the timeline. The card only fades.

Code & diagrams

A card that enters and exits

Show and hide it, pause during the exit, then try Hide, then show at once, and Remove from the page.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

allow-discrete: true
Remove a card after its exit

Add a card, then remove it: the exit plays and only then does the element leave the DOM. Switch on Reduced motion: only the movement goes, the fade stays, and the card is removed when the fade ends.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

cards: 0
A popover with an exit, and one without

Open each popover and close it by clicking outside or pressing Escape. Pause during the close and compare the tracks.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

popover support: true

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Leave out the display transition

In the card example's CSS delete , display 400ms allow-discrete.

terminal
$ Show the card, wait, press Hide
── what you'll see ──
One frame (17 ms) after Hide the card is already `display: none` with opacity 0. The `opacity` and `translate` tracks exist but are `idle`: nothing is rendered to show them.

Break #2

Forget @starting-style

Delete the @starting-style { ... } rule.

terminal
$ Press Show
── what you'll see ──
The card appears at full opacity at 0 ms. No transition tracks are created.

Myth vs fact

Myth

A CSS exit works when you call remove() on the element.

Fact

Once removed there is nothing to render. Add the exit class first, wait for the animations, then remove.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    @starting-style also runs on the first render of the page. Every element that has one will animate in at load, which may or may not be what you want. Put the rule inside the state that needs it.

  • ▸

    Entry with @starting-style and transition animates only the properties you list. Animating translate and scale (separate properties) instead of transform leaves transform free for FLIP, so the gallery's entrance and its FLIP slide can run on the same element without fighting.

  • ▸

    Browsers that lack allow-discrete or @starting-style simply show and hide the element at once (Chrome 117 was first; Firefox and Safari followed, so check caniuse for your audience's versions). The animation is a progressive enhancement: the card still works.

Remember this

  1. 1

    The problem this solves: a CSS transition runs between two styles of an element that is already rendered. A new element, or one that goes from display: none to block, has no earlier style, so it just appears. And display: none cannot be animated by fading, because it is not a number: the browser flips it at once and the fade has nothing left to fade.

  2. 2

    **Entry with @starting-style.** The rule @starting-style { .card { opacity: 0; translate: 0 14px; } } gives the first style the element has when it starts rendering. The browser then transitions from that to the normal style. It runs when the element is first rendered, when it is inserted, and when it leaves display: none. Chrome 117 shipped it.

  3. 3

    **Exit with transition-behavior: allow-discrete.** A discrete property (display, content-visibility, overlay) does not take part in a transition: it flips at once. With display 400ms allow-discrete in the transition list, the flip to none happens at the end of the 400 ms instead of at the start, so the opacity fade is visible. Chrome 117 shipped it too.

  4. 4

    **The top layer and overlay.** Popovers and <dialog> elements render in the top layer, above everything, and leave it the moment they close. overlay 400ms allow-discrete keeps them in the top layer for the exit, so the fade is not covered by page content. Add ::backdrop to the transitions to fade the dim background as well.

  5. 5

    A reversed exit is shorter. If the user shows the card again while it is half faded out, the browser reverses the running transition and shortens it in proportion to how far it got (the "reversing shortening factor"). In the example below, hiding at 200 ms into the entrance made a 303 ms exit for opacity, while the display transition still ran its 400 ms. For about 100 ms the card is invisible but still in the layout.

  6. 6

    Removing the element is a different case. el.remove() leaves no element to fade, so a CSS exit never runs. Add the exit class, wait for the animations to finish with getAnimations() and finished, then remove. If reduced motion removed the transitions, getAnimations() is empty and the removal happens at once.

Explain it without notes

01

Why does a fade-out of a display: none element not work without allow-discrete?

02

What does overlay do for a popover's exit?

Practice

01

Write the CSS for a dialog that fades and scales in and out, including the backdrop.

02

Write leave(el) so that it also works when reduced motion removes the transitions.

Trade-offs

  • ↔

    A CSS exit needs the element to stay in the layout until it ends. If neighbours must close the gap at the same time, use FLIP with a fade-out copy (Topic 10.2).

Done when you can

  • You can animate an entrance with @starting-style

  • You can keep display and overlay on for an exit

  • You can remove an element after its exit has finished

  • You can explain why a reversed transition is shorter