Symptom
A sticky bottom bar was supposed to hide (opacity/transform only, for CLS) whenever an in-flow panel scrolled into view, driven by:
new IntersectionObserver(([e]) => { hidden = e.isIntersecting; },
{ rootMargin: '0px 0px -64px 0px' }).observe(panel);Smooth scrolling worked. But window.scrollTo(0, y) (anchor jumps, test drivers, fast flicks) from a position where the panel was below the viewport to one where it was fully above produced no callback at all: instrumenting the IO constructor showed exactly one fire (the initial observation) and then silence. The element went below-viewport -> above-viewport without ever being presented in an intersecting frame, so isIntersecting stayed false -> false and IO reports only changes.
Any logic of the form "hidden while intersecting, and also hidden once scrolled past" (checking boundingClientRect.top < 0 in the callback) is equally dead code for jumps, because the callback never runs.
Fix
Make the intersection region one-sided so the two states collapse into one region with a single boundary:
new IntersectionObserver((entries) => {
hidden = entries[entries.length - 1].isIntersecting;
}, { rootMargin: '100000px 0px -64px 0px' }).observe(panel);The huge top margin extends the root box far above the viewport, so "intersecting" now means "the panel top has crossed viewportBottom - 64px" -- on screen OR scrolled past. Every scroll position maps to exactly one of two states separated by one boundary, so even a single-frame jump is a crossing and always fires. This also naturally reproduces docked-drawer semantics (stay hidden when scrolled past the panel).
Use entries[entries.length - 1], not ([e]): batched delivery can contain multiple crossings and only the last reflects current state.
Transfer
Applies to any IO-driven scroll-position threshold (show/hide FABs, reading-progress markers, docked bars): if both "before" and "after" states are non-intersecting, jumps are invisible to the observer. Encode the predicate as membership in a half-open region via asymmetric rootMargin instead of "is the sentinel on screen".