Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Entry and exit animations

A transition from the lesson "Transitions" animates a change of value. But an element that has just appeared had no "previous" value — there is nothing to transition from. And display: none does not animate at all: the element vanishes instantly. Modern CSS closes both gaps.

@starting-style: where to start

The @starting-style rule sets the styles an element starts from at the moment it appears. The browser takes them as the "previous" value and runs a transition to the regular styles. Press "Reset" above the example to see it again:

<style>
  .note {
    padding: 12px;
    background: honeydew;
    transition: opacity 1s, translate 1s;
  }
  @starting-style {
    .note { opacity: 0; translate: 0 20px; }
  }
</style>
 
<div class="note">I slid up smoothly</div>

Without @starting-style the block would just appear in place at once. Here translate is a separate shift property, the same as transform: translate().

A transition from display: none

display switches abruptly: there is no "half hidden" value. Such properties are called discrete. The keyword allow-discrete (in full — transition-behavior: allow-discrete) lets them take part in a transition: when hiding, display changes to none at the very end, when the opacity has already reached zero:

<style>
  .box {
    padding: 12px;
    margin-top: 8px;
    background: lavender;
    transition: opacity 0.5s, display 0.5s allow-discrete;
  }
  .box.hidden { display: none; opacity: 0; }
  @starting-style {
    .box { opacity: 0; }
  }
</style>
 
<button onclick="document.querySelector('.box').classList.toggle('hidden')">Show / hide</button>
<div class="box">I appear smoothly and disappear smoothly</div>

@starting-style handles appearing, and allow-discrete handles disappearing. Both are needed.

A dialog and a popover

<dialog> windows and popover blocks from the lesson "Disclosure widgets and dialogs" are also hidden with display: none. The scheme is the same; besides display, the overlay property is added to the transition — it keeps the window above the page while the closing animation runs:

<style>
  body { min-height: 200px; }
  dialog {
    opacity: 0;
    scale: 0.9;
    transition: opacity 0.3s, scale 0.3s, display 0.3s allow-discrete, overlay 0.3s allow-discrete;
  }
  dialog[open] { opacity: 1; scale: 1; }
  @starting-style {
    dialog[open] { opacity: 0; scale: 0.9; }
  }
</style>
 
<button onclick="document.querySelector('#hi').showModal()">Open the window</button>
<dialog id="hi">
  <p>The window opened smoothly.</p>
  <form method="dialog"><button>Close</button></form>
</dialog>

For a popover, write [popover]:popover-open instead of dialog[open].

Height auto: interpolate-size

Another long-standing problem is a transition to height: auto. The browser could not compute in-between values between a number and the word auto, and the block opened with a jump. The line interpolate-size: allow-keywords on :root allows such transitions on the whole page. Hover over the block:

<style>
  :root { interpolate-size: allow-keywords; }
  .more {
    height: 24px;
    overflow: hidden;
    padding: 8px;
    background: lightyellow;
    transition: height 0.4s;
  }
  .more:hover { height: auto; }
</style>
 
<div class="more">Hover — I will open.<br>The second line.<br>The third line.<br>The fourth line.</div>

At the time of writing interpolate-size works in Chrome and Edge; in other browsers the block simply opens at once, without animation. Check current support on caniuse.com.

Support

@starting-style and allow-discrete are supported in all major browsers since 2024. The overlay property is not everywhere: without it the window may close without animation, but it still closes. All these techniques are progressive enhancement: an old browser shows and hides the element instantly, as before.

Common mistake

@starting-style before the main rule. The selectors have the same specificity, so the lower one wins: the regular style overrides the starting one, and the element appears without animation. Press "Reset":

<style>
  @starting-style { .bad { opacity: 0; } }
  .bad, .good { opacity: 1; padding: 8px; margin: 4px 0; background: mistyrose; transition: opacity 1.5s; }
  @starting-style { .good { opacity: 0; } }
</style>
 
<div class="bad">Starting style above — I appear at once</div>
<div class="good">Starting style below — I fade in smoothly</div>

Write @starting-style after the element's rule, or nest it right inside the rule.

Thanks for reading!

Donate to the site — we work hard for you.