demo · v138

Nested scrollers

Two scrollers, one inside the other. scrollIntoView({ container: "all" }) scrolls every ancestor; "nearest" stops at the closest scroll-container. The new container option finally gives you a clean choice — until v138 the only escape hatch was a hand-rolled scrollBy() loop up the parent chain.

Unflagged in Chrome 138. Element.scrollIntoView({ container: 'all' | 'nearest' }) joins the existing block, inline, behavior options.
probing container option…
outer scroller — top spacer
inner scroller — start. The target sits 300px down inside this scroller.

target element

Calling target.scrollIntoView({container: "nearest"}) only moves the inner scroller. Calling target.scrollIntoView({container: "all"}) also brings the outer scroller into alignment.

inner scroller — end
outer scroller — bottom spacer

Call log

The API

// Pre-v138 — every ancestor scrolls, no opt-out
target.scrollIntoView();

// v138
target.scrollIntoView({
  container: 'all',      // scroll every scroll ancestor up to root (default behaviour)
  block: 'center',
});

target.scrollIntoView({
  container: 'nearest',  // stop at the nearest scroll-container — don't disturb outer scrollers
  block: 'center',
});

What's happening

  1. By default scrollIntoView walks all the way up: every scroll ancestor gets adjusted to bring the target into view. Fine for a single scroller; bad UX for modals, popovers, nested chat threads.
  2. container: 'nearest' stops at the closest scrollable ancestor. The inner scroller moves; the outer page stays where it is.
  3. This was the missing primitive for nested-scroll UIs — chat panes inside a modal, sub-lists inside a side-panel, focused list items inside a tab.
  4. Try the two options against the same target with different starting positions. Notice the outer scroller position changes only with "all".

see also