stickyObserver
Marks a sticky element while it is pinned so CSS can style the stuck state.
Scroll this page. Each bar below travels with its section until it
reaches the top of the viewport — there
position: sticky pins it, the script flips
is-stuck, and the bar gains an elevated surface
with a hairline and a shadow — a state pure CSS cannot
select, because there is no :stuck pseudo-class. Three
independent instances run on this page: auto-injected sentinel,
author-placed sentinel, and a bar inside a scroll container.
Keep scrolling — this text exists so the section is long enough
for the bar above to actually pin. That bar carries nothing but
data-js-behavior="stickyObserver": the tiny sentinel
element the observer watches was injected by the script right
before the bar. Open the inspector and you will find it — an
empty, zero-size <div> with
aria-hidden="true" that exists only while JavaScript
runs.
The pinning itself never depended on JavaScript. The bar is a
plain position: sticky element, so with scripting
disabled it would still attach to the viewport top at exactly the
same scroll position — it would just keep its transparent in-flow
look, and no sentinel would exist at all. What the script adds is
purely the missing piece of information: a class that says "this
element is pinned right now", which the stylesheet turns into the
elevated surface.
There is no scroll listener running and nothing is measured per frame: an IntersectionObserver reports only the moments the sentinel crosses the viewport edge. Between two flips this page costs nothing. Scroll on — this bar unpins where its section ends, and the second instance takes over.
This paragraph is the sentinel of bar two —
marked with an id and referenced via
data-sticky-sentinel="#surfaceTrigger". Watch
closely: the bar pins the moment it reaches the viewport top, but
it stays transparent while these lines slide underneath it. Only
once this whole paragraph has scrolled past does the surface fade
in.
That is the point of the second mode: the trigger is decoupled
from the element. An author-placed sentinel can sit anywhere in
the layout — a hero section whose passing should condense the
site header, a marker deep inside an article — and any element
works, not just an empty helper. It is also the escape hatch for
markup where an injected sibling would not be free: inside a
flex or grid container with a gap the extra child
occupies a slot, and list or table markup does not allow a
stray <div> between its items.
Both instances on this page run independently — each owns its sentinel and its observer, and several elements could even share one sentinel by pointing at the same selector. Scroll back to the top now and watch the hand-over in reverse: as each trigger re-enters the viewport its bar loses the class, the surface fades out, and the bar is an ordinary part of the page flow again.
The third bar does not care about the page scroll at all. It
lives inside the framed overflow: auto box below
and pins against its top edge —
data-sticky-root="#demoScrollBox" hands that box to
the observer as its root, because an element root is the only
way the class can flip against the container edge the bar
actually sticks to. Scroll inside the box to
see it.
Scroll this box. The bar above is an ordinary
position: sticky element whose containing scroller
happens to be this box, not the page — so it pins the moment
the box's content starts moving underneath it, while the page
itself stands still.
The wiring is the same as everywhere else: the script injected a zero-size sentinel right before the bar, and the observer reports the moments that sentinel crosses the box's top edge. The only difference is the observer's root — the box instead of the viewport.
Scroll back up inside the box and the bar releases: the sentinel re-enters the box, the class is removed, and the surface fades out again — completely independent of the two instances above.