scrollIntoView Has Options (and Focus Can Skip the Scroll)
Scrolling code has a reputation for jank that it earns one default at a time: elements slammed to the exact top edge, pages yanked around by focus, cleanup that runs before scrolling actually stops. Each has a precise fix.
scrollIntoView: use the options object
// The 2010 version - target glued to viewport top, likely under your sticky header
el.scrollIntoView();
// The version that looks intentional
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
block controls vertical alignment: 'start', 'center', 'end', or my favorite - 'nearest', which scrolls the minimum distance to bring the element into view and does nothing if it's already visible. For "make sure the selected row is on screen" in lists, nearest is almost always the right answer:
selectedRow.scrollIntoView({ block: 'nearest' }); // no jump if already visible
Sticky-header clipping when using 'start' is solved in CSS, not JS - scroll-margin-top offsets all programmatic scrolls too.
focus() moves the page - unless you tell it not to
input.focus({ preventScroll: true });
Focusing an off-screen element scrolls it into view by default. Correct for keyboard navigation; catastrophic for "focus the search field in the slide-out panel while it's still animating", which visibly yanks the page mid-transition. preventScroll gives you focus without motion, then you scroll deliberately (or not at all). Every polished drawer/modal implementation needs this one.
Knowing when scrolling ends: scrollend
Scroll events fire dozens of times through a scroll; there was never a clean "it stopped" - people debounce-timered it and hoped. Now:
container.addEventListener('scrollend', () => {
updateUrlWithVisibleSection(); // run once, after settling
snapIndicatorToActiveSlide();
});
It fires once when scrolling (including momentum and smooth-scroll animation) completes. Support: Chrome 114+, Firefox 109+; Safari only very recently - so feature-detect and keep the debounce fallback:
if ('onscrollend' in window) {
el.addEventListener('scrollend', done);
} else {
let t;
el.addEventListener('scroll', () => { clearTimeout(t); t = setTimeout(done, 120); }, { passive: true });
}
(Note the passive flag on the fallback - it matters for scroll performance.)
The rule that outranks all the APIs
Smooth-scroll only on direct user actions, honor prefers-reduced-motion (pass behavior: 'auto' when reduced), and never animate scroll on page load. Scroll position belongs to the user; these APIs are for the moments they've explicitly delegated it to you.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.