<h1 id="title">In&Out element transitions with @starting-style</h1> 
<button id="toggleTitle">Toggle [hidden] on title</button>

<p>Add an item below, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style"><code>@starting-style</code></a> describes the element before it's inserted into the page.</p> 

<section id="grid">
  <div></div>
</section>

<div>
  <button id="add">Add</button>
  <button id="remove">Remove</button>
  <button id="toggleContent">Toggle [hidden] on items</button>
</div>

<p>These buttons toggle the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/hidden">[hidden]</a> attribute on various elements, which sets display to none and opacity to 0. Thanks to <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/transition-behavior">transition-behavior</a>, none is delayed.</p> 

<p>It also works on dialog and popover elements</p>

<div>
  <button id="toggleDialog">Show a dialog</button>
  <button id="togglePopover">Show a popover</button>
</div>

<dialog id="dialogExample">
  <div>
    <h2>Are you sure?</h2>
    <button id="closeDialog">Close</button> 
  </div>
</dialog>

<pop-up popover id="popup">
  <h4>Pop!</h4>
</pop-up>
@import "https://unpkg.com/open-props/easings.min.css";

/* use an anonymous layer to demote the effect */
@layer {
  * {
    @media (prefers-reduced-motion: no-preference) {
      transition: 
        opacity .5s ease-in, 
        scale .5s ease-in,
        display .5s ease-in;
      /* key to transitioning out */
      transition-behavior: allow-discrete;
    }

    /* stage enter */
    /* key to transitioning in */
    @starting-style { 
      opacity: 0; 
      scale: 1.1; 
    }

    /* stage exit */
    /* use your own technique here */
    &[hidden], 
    dialog:not(:modal), 
    &[popover]:not(:popover-open) { 
      opacity: 0;
      scale: .9;

      /* hidden sets display: none, but loses easily */
      display: none !important; 
      
      /* faster leaving the stage then entering */
      transition-duration: .4s;
      transition-timing-function: var(--ease-out-5);
    }
  }
}








@layer support {
  * {
    box-sizing: border-box;
    margin: 0;
  }

  html {
    block-size: 100%;
    color-scheme: dark light;
  }

  body {
    min-block-size: 100%;
    font-family: system-ui, sans-serif;

    display: grid;
    place-content: center;
    place-items: start;
    gap: 1rlh;
  }
  
  p, section, ul {
    max-inline-size: 40ch;
  }
  
  h1 {
    max-inline-size: 15ch;
  }
  
  ul, h1 {
    text-wrap: balance;
  }
  
  section {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    
    > div {
      background: CanvasText;
      inline-size: 10ch;
      aspect-ratio: 1;
    }
  }
  
  dialog, [popover] {
    margin: auto;
  }
  
  dialog > div {
    display: grid;
    place-items: end;
    gap: 1rlh;
  }
}
// these functions demonstrate mutations
// so we can observe how the CSS helps transition

add.onclick = () =>
  grid.appendChild(document.createElement('div'))

remove.onclick = async () => {
  let last = grid?.lastElementChild
  last.hidden = true
  
  await onTransitionsEnded(last)
  last.remove()
}

toggleTitle.onclick = () =>
  title.hidden = !title.hidden

toggleContent.onclick = () =>
  grid.hidden = !grid.hidden

toggleDialog.onclick = () =>
  dialogExample.showModal()

closeDialog.onclick = async () =>
  dialogExample.close()

togglePopover.onclick = () =>
  popup.showPopover()



function onTransitionsEnded(node) {
  return Promise.allSettled(
    node.getAnimations().map(animation =>
      animation.finished))
}
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.