v149 · css · shipped
CSS scroll-state() Container Queries
Style elements based on their scroll container's live state — whether a sticky item is stuck, a snap target is currently snapped, or a scroll container has overflowed — without any JavaScript polling.
at a glance
| Shipped in | Chrome 149 (desktop + Android) |
|---|---|
| Status | Enabled by default |
| Flag | None |
| Standards position | Spec in CSS Conditional Rules Level 5 |
| Spec | CSS Conditional Rules Level 5 §scroll-state |
| ChromeStatus | chromestatus.com/feature/5072263730167808 |
why it exists
Before scroll-state(), reacting to sticky headers, snap targets, or scrollable overflow required JavaScript observers — IntersectionObserver for stickiness, MutationObserver hacks for snap, ResizeObserver for overflow. These all involve layout recalculation callbacks that race with painting. scroll-state() exposes three commonly-needed scroll signals as CSS custom container properties, letting the browser style them synchronously during its own paint pass with no script overhead.
shape of the feature
Step 1: establish a scroll-state container
Add container-type: scroll-state to the element you want to query. You can combine it with a container-name for targeting.
.sticky-header {
position: sticky;
top: 0;
container-type: scroll-state;
container-name: sticky-nav;
}
Step 2: query with @container scroll-state()
Use the @container scroll-state() at-rule inside the container or on its descendants.
Supported scroll-state features
| Feature | Values | Matches when… |
|---|---|---|
stuck |
none | top | right | bottom | left | block-start | block-end | inline-start | inline-end |
The container has position: sticky and is visually shifted to stay inside its sticky view rectangle on that edge. none matches when not stuck on any edge. |
snapped |
none | x | y | block | inline | both |
The container is a snap target that is (or would be) snapped to its scroll container on the given axis. none when not snapped. |
scrollable |
none | top | right | bottom | left | block-start | block-end | inline-start | inline-end | x | y | block | inline | both |
The container is a scroll container with clipped content in that direction that the user can scroll to. Does not match for overflow: hidden containers. |
example: style a sticky header when stuck
Show a box-shadow only when the header is actually pinned to the top — no JavaScript.
.page-header {
position: sticky;
top: 0;
container-type: scroll-state;
background: white;
transition: box-shadow 0.2s;
}
@container scroll-state(stuck: top) {
.page-header {
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
}
example: highlight the currently snapped carousel item
.carousel-item {
container-type: scroll-state;
opacity: 0.6;
transition: opacity 0.2s;
}
@container scroll-state(snapped: x) {
.carousel-item {
opacity: 1;
}
}
example: show a "more content below" indicator
.article {
overflow-y: auto;
max-height: 400px;
container-type: scroll-state;
container-name: article-scroller;
}
.scroll-hint {
display: none;
}
@container article-scroller scroll-state(scrollable: bottom) {
.scroll-hint {
display: block; /* visible only when there's more to scroll down */
}
}
browser support
| Chrome | 149+ (enabled by default) |
|---|---|
| Edge | 149+ (tracking Chromium) |
| Firefox | No signal yet |
| Safari | No signal yet |