Scrollspy dots

A column of dots shows which full-height section you are in and jumps to any of them.

How it works

An IntersectionObserver with a narrow rootMargin watches a line across the middle of the screen. Whichever section crosses that line becomes active, and its dot grows and changes colour.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scrollspy dots</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; }

section { height: 100vh; background: var(--bg); display: grid; place-items: center; }
section h2 { font: 800 clamp(3rem, 10vw, 7rem)/1 system-ui, sans-serif; color: #fff; margin: 0; mix-blend-mode: overlay; }
.dots { position: fixed; right: 22px; top: 50%; transform: translateY(-50%); display: grid; gap: 14px; z-index: 5; }
.dots a { width: 12px; height: 12px; border-radius: 50%; background: rgba(255,255,255,.5); display: block; transition: transform .25s, background .25s; }
.dots a.on { background: #fff; transform: scale(1.6); }
</style>
</head>
<body>
<nav class="dots" id="dots"></nav>
<section style="--bg:#ff6b6b"><h2>Coral</h2></section>
<section style="--bg:#feca57"><h2>Saffron</h2></section>
<section style="--bg:#48dbfb"><h2>Sky</h2></section>
<section style="--bg:#1dd1a1"><h2>Mint</h2></section>
<section style="--bg:#5f27cd"><h2>Violet</h2></section>
<section style="--bg:#222f3e"><h2>Ink</h2></section>
<script>
const sections = document.querySelectorAll('section');
const nav = document.getElementById('dots');
sections.forEach((s, i) => {
  s.id = 's' + i;
  const a = document.createElement('a');
  a.href = '#s' + i;
  a.setAttribute('aria-label', s.textContent);
  nav.appendChild(a);
});
const dots = nav.querySelectorAll('a');
const io = new IntersectionObserver(entries => {
  entries.forEach(e => {
    if (e.isIntersecting) {
      const i = [...sections].indexOf(e.target);
      dots.forEach((d, j) => d.classList.toggle('on', i === j));
    }
  });
}, { rootMargin: '-50% 0px -50% 0px' });
sections.forEach(s => io.observe(s));
</script>
</body>
</html>

More progress effects