Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

scrollIntoView Has Options (and Focus Can Skip the Scroll)

C
Carlo Fontanos
· 2 min read

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.

C
Written by Carlo Fontanos

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.

Message sent!

Your details are only used to reply to you.

Keep reading