Horizontal pin
Scrolling down slides a row of panels sideways, then lets the page carry on vertically.
A pinned timeline slides past while a line draws along it and each year lights up as it is reached.
Same pinning technique as a horizontal gallery, plus two extras: the connecting line’s scaleX follows overall progress, and each milestone becomes active once the line has passed its x position.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Horizontal timeline</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; }
body { background: #fff8f0; color: #2d1e2f; }
.sticky { position: sticky; top: 0; height: 100vh; overflow: hidden; display: flex; flex-direction: column; justify-content: center; }
.sticky h1 { padding: 0 6vw; font-size: clamp(1.8rem, 4vw, 3rem); margin: 0 0 8vh; }
.rail { position: relative; display: flex; gap: 0; padding: 0 6vw; width: max-content; will-change: transform; }
.line { position: absolute; left: 6vw; right: 6vw; top: 9px; height: 4px; background: #eadfd5; }
.line b { display: block; height: 100%; background: #e4572e; transform-origin: 0; transform: scaleX(0); }
.m { width: 300px; position: relative; padding-top: 40px; opacity: .35; transition: opacity .4s; }
.m::before { content: ''; position: absolute; top: 0; left: 0; width: 22px; height: 22px; border-radius: 50%; background: #eadfd5; transition: background .4s, transform .4s; }
.m span { font: 800 2.4rem system-ui, sans-serif; }
.m p { max-width: 24ch; font-size: 1.05rem; }
.m.on { opacity: 1; }
.m.on::before { background: #e4572e; transform: scale(1.2); }
.end { height: 60vh; display: grid; place-content: center; }
</style>
</head>
<body>
<section class="pin" id="pin">
<div class="sticky">
<h1>A short history of scrolling</h1>
<div class="rail" id="rail">
<div class="line"><b id="line"></b></div>
<div class="m"><span>1991</span><p>First web page. You scroll with arrow keys.</p></div>
<div class="m"><span>1996</span><p>The wheel mouse arrives on desktops.</p></div>
<div class="m"><span>2003</span><p>Two-finger trackpad scrolling.</p></div>
<div class="m"><span>2007</span><p>Inertial scrolling on phones.</p></div>
<div class="m"><span>2011</span><p>Parallax sites take over.</p></div>
<div class="m"><span>2016</span><p>IntersectionObserver ships.</p></div>
<div class="m"><span>2023</span><p>Scroll-driven animations in CSS.</p></div>
</div>
</div>
</section>
<div class="end">That’s the timeline.</div>
<script>
const pin = document.getElementById('pin');
const rail = document.getElementById('rail');
const line = document.getElementById('line');
const ms = rail.querySelectorAll('.m');
function size() {
pin.style.height = (rail.scrollWidth - innerWidth + innerHeight * 1.5) + 'px';
update();
}
function update() {
const r = pin.getBoundingClientRect();
const dist = Math.max(0, rail.scrollWidth - innerWidth);
const p = Math.min(1, Math.max(0, -r.top / (r.height - innerHeight)));
rail.style.transform = 'translate3d(' + (-p * dist) + 'px,0,0)';
line.style.transform = 'scaleX(' + p.toFixed(4) + ')';
const lineW = line.parentElement.offsetWidth * p;
ms.forEach(m => m.classList.toggle('on', m.offsetLeft - ms[0].offsetLeft <= lineW + 1));
}
addEventListener('scroll', update, { passive: true });
addEventListener('resize', size);
size();
</script>
</body>
</html>