Pattern

Current section

The rail always shows where you are in a long page, and lets you jump.

01

Watch it

Scrolling Management README on desktop, then jumping to a section. Recorded from the live code.
02

Rules

Which section is current
The last section whose top has passed 30% of the viewport height.
Marker
One 2px bar in the marker colour slides to the current entry: transform and height, 250ms on the system ease. The link gets aria-current="true".
Tablet
The rail becomes a sticky number row; the current number gets a 2px underline instead of the sliding bar.
Phone
No rail. Sections still carry their numbers.
Without JavaScript
The rail is a plain list of anchor links; no marker.
Performance
One scroll listener, throttled to animation frames; recalculated on resize.
03

Code

From site/assets/js/site.js, lines 14 to 47.

// ------------------------------------------------------------ rail marker
const rail = document.querySelector('.rail');
if (rail) {
    const links = [...rail.querySelectorAll('a[href^="#"]')];
    const sections = links.map((a) => document.getElementById(a.hash.slice(1))).filter(Boolean);
    const mark = rail.querySelector('.rail-mark');
    const list = rail.querySelector('ol'); // the marker sits in the rail, the links in the list
    const set = (i) => {
        links.forEach((a, j) => (j === i ? a.setAttribute('aria-current', 'true') : a.removeAttribute('aria-current')));
        if (mark && links[i]) {
            mark.hidden = false;
            mark.style.setProperty('--rail-y', links[i].offsetTop + (list ? list.offsetTop : 0) + 'px');
            mark.style.setProperty('--rail-h', links[i].offsetHeight + 'px');
        }
    };
    const pick = () => {
        // The active section is the last one whose top has passed 30% of the viewport.
        const line = innerHeight * 0.3;
        let i = 0;
        sections.forEach((s, j) => {
            if (s.getBoundingClientRect().top <= line) i = j;
        });
        set(i);
    };
    let queued = false;
    addEventListener('scroll', () => {
        if (queued) return;
        queued = true;
        requestAnimationFrame(() => { queued = false; pick(); });
    }, { passive: true });
    addEventListener('resize', pick, { passive: true });
    pick();
}