<section>
  <h1>Scroll a bit</h1>
</section>
<section>
  <h1>Feel out when scroll has ended</h1>
</section>
<section class="no-support-message">
  <h1>This browser doesn't support <code>scrollend</code>.</h1>
</section>
@layer demo {
  body > section {
    block-size: 100vh;
    display: grid;
    place-items: center;
  }
  
  .no-scrollend body > section:not(.no-support-message) {
    display: none;
  }
  
  :not(.no-scrollend) .no-support-message {
    display: grid;
  }
}

@layer demo.toast {
  .gui-toast-group {
    position: fixed;
    z-index: 1;
    inset-block-end: 0;
    inset-inline: 0; 
    padding-block-end: 5vh;

    display: grid;
    justify-items: center;
    justify-content: center;
    gap: 1vh;

    /* optimizations */
    pointer-events: none;
  }

  .gui-toast {
    --_duration: 3s;
    --_bg-lightness: 90%;
    --_travel-distance: 0;

    font-family: system-ui, sans-serif;
    color: white;
    background: deeppink;

    max-inline-size: min(25ch, 90vw);
    padding-block: .5ch;
    padding-inline: 1ch;
    border-radius: 3px;
    font-size: 1rem;

    will-change: transform;
    animation: 
      fade-in .3s ease,
      slide-in .3s ease,
      fade-out .3s ease var(--_duration);

    @media (prefers-color-scheme: dark) {
      color: white;
      --_bg-lightness: 20%;
    }

    @media (prefers-reduced-motion: no-preference) {
      --_travel-distance: 5vh;
    }
  }
}

@keyframes fade-in {
  from { opacity: 0 }
}

@keyframes fade-out {
  to { opacity: 0 }
}

@keyframes slide-in {
  from { transform: translateY(var(--_travel-distance, 10px)) }
}

@layer demo.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;
  }
}
// Event is at the bottom
// after the Toast component functions are ready

const hasScrollend = 'onscrollend' in window

if (!hasScrollend) {
  document.querySelector('html').classList.add('no-scrollend')
}

const init = () => {
  const node = document.createElement('section')
  node.classList.add('gui-toast-group')

  document.firstElementChild.insertBefore(node, document.body)
  return node
}

const createToast = text => {
  const node = document.createElement('output')
  
  node.innerText = text
  node.classList.add('gui-toast')
  node.setAttribute('role', 'status')
  node.setAttribute('aria-live', 'polite')

  return node
}

const addToast = toast => {
  const { matches:motionOK } = window.matchMedia(
    '(prefers-reduced-motion: no-preference)'
  )

  Toaster.children.length && motionOK
    ? flipToast(toast)
    : Toaster.appendChild(toast)
}

const Toast = text => {
  let toast = createToast(text)
  addToast(toast)

  return new Promise(async (resolve, reject) => {
    await Promise.allSettled(
      toast.getAnimations().map(animation => 
        animation.finished
      )
    )
    Toaster.removeChild(toast)
    resolve() 
  })
}

// https://aerotwist.com/blog/flip-your-animations/
const flipToast = toast => {
  // FIRST
  const first = Toaster.offsetHeight

  // add new child to change container size
  Toaster.appendChild(toast)

  // LAST
  const last = Toaster.offsetHeight

  // INVERT
  const invert = last - first

  // PLAY
  const animation = Toaster.animate([
    { transform: `translateY(${invert}px)` },
    { transform: 'translateY(0)' }
  ], {
    duration: 150,
    easing: 'ease-out',
  })

  animation.startTime = document.timeline.currentTime
}

const Toaster = init()

document.onscrollend = event => {
  Toast('scroll end')
}
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.