Snap stories

Full-screen story slides that snap into place, with segmented progress bars across the top.

How it works

CSS scroll snap does the paging. An IntersectionObserver finds the slide that fills the screen and fills every progress segment up to it, while the current segment animates its fill so the active slide is clear.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Snap stories</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; }
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; -webkit-font-smoothing: antialiased; }

html { scroll-snap-type: y mandatory; }
body { color: #fff; }
.story { height: 100vh; scroll-snap-align: start; scroll-snap-stop: always; background: var(--bg);
  display: flex; flex-direction: column; justify-content: flex-end; padding: 0 7vw 12vh; }
.story h2 { font: 900 clamp(2.6rem, 10vw, 5.5rem)/1 system-ui, sans-serif; letter-spacing: -.04em; margin: 0 0 .2em; }
.story p { font-size: 1.3rem; margin: 0; }
.bars { position: fixed; top: 14px; left: 14px; right: 14px; display: flex; gap: 6px; z-index: 5; }
.bars i { flex: 1; height: 4px; border-radius: 2px; background: rgba(255,255,255,.35); overflow: hidden; }
.bars b { display: block; height: 100%; width: 100%; background: #fff; transform-origin: 0; transform: scaleX(0); }
.bars b.done { transform: scaleX(1); }
.bars b.now { animation: fill 3s linear forwards; }
@keyframes fill { to { transform: scaleX(1); } }
</style>
</head>
<body>
<div class="bars" id="bars"></div>
<section class="story" style="--bg:linear-gradient(160deg,#ff9a8b,#ff6a88)"><h2>Morning run</h2><p>5.2 km before coffee.</p></section>
<section class="story" style="--bg:linear-gradient(160deg,#a18cd1,#fbc2eb)"><h2>Desk</h2><p>Shipping the scroll update.</p></section>
<section class="story" style="--bg:linear-gradient(160deg,#84fab0,#8fd3f4)"><h2>Lunch</h2><p>Noodles, obviously.</p></section>
<section class="story" style="--bg:linear-gradient(160deg,#fccb90,#d57eeb)"><h2>Golden hour</h2><p>Best light of the day.</p></section>
<section class="story" style="--bg:linear-gradient(160deg,#30cfd0,#330867)"><h2>Night</h2><p>See you tomorrow.</p></section>
<script>
const stories = [...document.querySelectorAll('.story')];
const bars = document.getElementById('bars');
stories.forEach(() => bars.insertAdjacentHTML('beforeend', '<i><b></b></i>'));
const fills = bars.querySelectorAll('b');
const io = new IntersectionObserver(entries => entries.forEach(e => {
  if (!e.isIntersecting) return;
  const idx = stories.indexOf(e.target);
  fills.forEach((f, i) => {
    f.classList.remove('now');
    f.classList.toggle('done', i < idx);
    if (i === idx) { void f.offsetWidth; f.classList.add('now'); }
  });
}), { threshold: 0.6 });
stories.forEach(s => io.observe(s));
</script>
</body>
</html>

More snap effects