Scrolling

Scrolling is one of the few things a user does constantly without thinking about it. Every flick of a trackpad, every drag of a scrollbar, every arrow-key press moves the viewport. The scroll event is how your code finds out.

Once you can observe the scroll position, a lot of familiar UI patterns open up:

  • Reveal or hide a sticky header, a “back to top” button, or a floating toolbar depending on how far down the page someone is.
  • Fetch and append the next batch of items when the reader nears the bottom — the “infinite scroll” pattern you see on feeds and search results.
  • Drive progress indicators, parallax effects, or scroll-linked animations.

Here’s the smallest useful example. It reads the current vertical scroll offset and writes it into an element on every scroll:

window.addEventListener('scroll', function() {
  document.getElementById('scrollDepth').innerHTML = window.pageYOffset + 'px';
});

In action:

Current scroll = scroll the window

The scroll event fires on the window for the whole page, and it also fires on any individual element that has its own scrollbar (an element with overflow: scroll or overflow: auto whose content is taller or wider than its box).

Because a live page can be awkward to scroll inside a small preview, the demo below gives an element its own scrollbar and reports its scrollTop — the element-level counterpart to window.scrollY. Scroll inside the box and watch the readout track your position:

interactiveReading an element's scroll position

A scroll offset only becomes useful once you turn it into something visible. Scroll progress bars — the thin line that fills as you move down an article — are the classic case. Convert the current offset into a percentage of the total scrollable distance and paint it:

interactiveA scroll-linked progress bar
scrolled away
scrolled away
viewport
below the fold
120px of content sits above the viewport’s top edge
window.scrollY reads 120
Scroll further down and the number grows; scroll back to the top and it returns to 0.
pageYOffset measures how far the document has scrolled up past the top of the viewport.

Why scroll handlers must stay cheap

The scroll event can fire a lot — many times per second during a fast flick. Whatever you put in the handler runs on that hot path, so heavy work there makes scrolling feel sluggish.

Two habits keep things smooth:

  • Read, don’t thrash. Reading scrollY is cheap. Repeatedly writing to the DOM and forcing layout recalculation inside the handler is not.
  • Throttle or debounce when the reaction doesn’t need to happen on every single tick — for example, “load more when near the bottom” only needs to check occasionally.

Prevent scrolling

Say you want to make something unscrollable — maybe a modal is open and the page behind it should stay put. How?

The instinct is to call event.preventDefault() inside the scroll handler. That doesn’t work. The scroll event is a report, not a request: it fires after the browser has already moved the viewport. Cancelling it is like hitting the brakes after you’ve already stopped — there’s nothing left to prevent.

user action
wheel / key / drag
viewport moves
(already happened)
scroll fires
preventDefault = no-op
The scroll event arrives after the movement is done, so preventDefault has nothing left to cancel.

The trick is to cancel the event that causes the scroll, before the browser acts on it. Those source events fire earlier in the chain and are cancelable. For example, the keydown event for PageUp and PageDown:

// Block scrolling triggered by the PageUp / PageDown keys.
window.addEventListener('keydown', function(event) {
  // 33 = PageUp, 34 = PageDown
  if (event.key === 'PageUp' || event.key === 'PageDown') {
    event.preventDefault();
  }
});

Add a handler like this to the causing event and call event.preventDefault() inside it, and the scroll never starts.

Because of that, the reliable way to make something unscrollable is CSS, not JavaScript. The overflow property controls whether an element scrolls at all:

/* Freeze the whole page — a common trick while a modal is open. */
body {
  overflow: hidden;
}

With overflow: hidden, the element simply has no scroll mechanism, so no input can move it. There’s no event to cancel because there’s no scroll to begin with.

Try it: the button toggles the box between overflow: auto and overflow: hidden. When it’s locked, no amount of wheeling, dragging, or arrow-keying will budge the content — exactly the behaviour you want behind an open modal.

interactiveLocking scroll with overflow: hidden
input event
keydown, wheel…
← preventDefault() stops it here
browser scrolls
viewport moves
← overflow:hidden removes this stage entirely
scroll event
too late to cancel
← report only
Where each technique sits in the pipeline from input to painted scroll.

The tasks that follow put onscroll to work — showing a “back to top” button, building endless page loading, and similar patterns. Read through them to see the event applied.