sections fade transition on scroll

by davidxmartins

HTML

<main id="stack">
    <section class="panel">
      <img src="https://cdn.slidemodel.com/wp-content/uploads/23212-01-8-step-arrows-circular-diagram-powerpoint-template-16x9-2.jpg" alt="Orange placeholder">
    </section>
    <section class="panel">
      <img src="https://cdn.slidemodel.com/wp-content/uploads/23212-01-8-step-arrows-circular-diagram-powerpoint-template-16x9-4.jpg">
    </section>
    <section class="panel">
      <img src="https://cdn.slidemodel.com/wp-content/uploads/23212-01-8-step-arrows-circular-diagram-powerpoint-template-16x9-8.jpg" alt="Blue placeholder">
    </section>
  </main>

CSS

html, body {
  height: 100%;
  margin: 0;
  font-family: system-ui, sans-serif;
  background: #000; /* prevents white flashes during fade */
}

main#stack {
  position: relative;
  height: 300svh; /* one full viewport height per section */
}

.panel {
  position: fixed;
  inset: 0;
  opacity: 0;
  transition: opacity 0.6s ease;
}

.panel img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Default first visible */
.panel:first-child {
  opacity: 1;
  z-index: 1;
}

/* Accessibility */
@media (prefers-reduced-motion: reduce) {
  .panel { transition: none; opacity: 1; }
}

JavaScript

(() => {
  const panels = Array.from(document.querySelectorAll('.panel'));
  const step = window.innerHeight; // each section is one viewport tall

  window.addEventListener('scroll', () => {
    const scroll = window.scrollY;
    const index = Math.floor(scroll / step);
    const progress = (scroll % step) / step; // 0 to 1 inside each section

    panels.forEach((p, i) => {
      let opacity = 0;
      if (i === index) {
        opacity = 1 - progress;       // fade out current
      } else if (i === index + 1) {
        opacity = progress;           // fade in next
      }
      p.style.opacity = opacity;
      p.style.zIndex = i;             // maintain order
    });
  });
})();