Sticky & snapped element styling without JavaScript
Styling elements differently when they become stuck (sticky) or snapped used to require JavaScript scroll event listeners. scroll-state() container queries let CSS respond to scroll-related states directly.
2const header = document
3 .querySelector('.header');
4const offset = header
5 .offsetTop;
6
7window.addEventListener(
8 'scroll', () => {
9 header.classList
10 .toggle('stuck',
11 window.scrollY
12 >= offset);
13 }
14);
2 container-type: scroll-state;
3}
4
5@container scroll-state(
6 stuck: top
7) {
8 .header { box-shadow: 0 2px 8px #0002; }
9}
No scroll listeners
The browser tracks stuck and snapped states internally. No scroll event handlers, no requestAnimationFrame, no jank.
Multiple states
Query stuck (top, bottom, left, right), snapped (x, y, inline, block), and overflowing states — all with pure CSS.
Container query model
Uses the familiar @container syntax. If you know container queries, you already know how to use scroll-state queries.
How it works
Sticky headers that add a shadow when stuck, or carousels that highlight the active slide — these patterns always required JavaScript. You'd listen to scroll events, calculate positions, and toggle classes. This causes layout thrashing, jank, and race conditions.
scroll-state() container queries let CSS detect scroll-related states natively. Set container-type: scroll-state on the scrolling ancestor, then query it: @container scroll-state(stuck: top) matches when a child is stuck to the top. @container scroll-state(snapped: x) matches the currently snapped element. The browser handles all the tracking with zero JavaScript.