Snap focus list

A list snaps each item to the centre and enlarges whichever item is in focus.

How it works

scroll-snap-align: center does the snapping. On scroll, each item’s distance from the viewport centre sets its scale and opacity, so the focused item is large and the rest fall away, like a wheel picker.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Snap focus list</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 { background: #1a1423; color: #f6e8ea; }
.wheel { padding: 40vh 0; display: grid; justify-items: center; }
.item {
  height: 20vh; display: flex; align-items: center; scroll-snap-align: center;
  font: 800 clamp(2rem, 8vw, 4.5rem)/1 system-ui, sans-serif; letter-spacing: -.02em;
  will-change: transform;
}
</style>
</head>
<body>
<main class="wheel" id="wheel"></main>
<script>
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const wheel = document.getElementById('wheel');
months.forEach(m => wheel.insertAdjacentHTML('beforeend', '<div class="item">' + m + '</div>'));
const items = wheel.querySelectorAll('.item');
function update() {
  const mid = innerHeight / 2;
  items.forEach(it => {
    const r = it.getBoundingClientRect();
    const d = Math.min(1, Math.abs(r.top + r.height / 2 - mid) / (innerHeight * 0.45));
    it.style.transform = 'scale(' + (1 - d * 0.5).toFixed(3) + ')';
    it.style.opacity = (1 - d * 0.8).toFixed(2);
    it.style.color = d < 0.15 ? '#ff7aa2' : '';
  });
}
addEventListener('scroll', update, { passive: true });
update();
</script>
</body>
</html>

More snap effects