scroll-state:让样式读出容器此刻的滚动态
一个 position: sticky 的导航栏有没有正贴住顶边、一个 carousel 当前吸附在哪张、一个滚动框还能不能继续往下滚——这些「滚动态」过去只能靠 JS 监听 scroll / IntersectionObserver 反复计算。Scroll-State 容器查询(CSS Conditional Rules Level 5)把它们做成声明式的:给容器声明 container-type: scroll-state,后代就能用 @container scroll-state(...) 按状态换样式,零脚本。
1 · 三种可查询的状态
| 查询 | 问的是 | 取值 |
|---|---|---|
stuck |
这个 sticky 容器当前贴住了哪条边(还是已被滚走、不再吸附)。 |
none / top / bottom / left / right / block-start… |
snapped |
作为 snap-area 的这个容器,是否正是滚动容器当前吸附到的那个,在哪条轴上。 | none / x / y / block / inline / both |
scrollable |
这个滚动容器朝某个方向还有没有可滚的内容(用来做边缘渐隐提示)。 | none / top / bottom / left / right / block-start… |
关键点:container-type: scroll-state 设在被查询的那个容器上(sticky 元素 / snap-area / 滚动框本身),而 @container scroll-state(...) 命中的样式作用于它的后代——和普通容器查询一样,容器自身不能直接被自己的查询命中。scroll-state 不像
size 那样建立尺寸 containment,因此不影响布局。
2 · stuck:sticky 贴住顶边时变身
下框里的导航栏是 position: sticky。向下滚动让它贴住顶边,看它从「圆角软底」收紧成「贴顶横条」——纯 CSS,不监听 scroll:
stuck 的 CSS 骨架:
.nav {
position: sticky;
top: 0;
container-type: scroll-state;
container-name: nav;
}
@container nav scroll-state(stuck: top) {
.nav-inner {
background: var(--accent);
border-radius: 10px;
color: #fff;
}
}
3 · snapped:吸附到正中那张就点亮
横向 carousel 里每张卡片既是 snap-area、又声明了 container-type: scroll-state。滚动容器吸附到哪张,哪张就用 scroll-state(snapped: inline) 把自己放大点亮——和
CSS Animation 的 view() 版轮播异曲同工,但这里靠的是「吸附态」而非「视口进度」:
snapped 的 CSS 骨架:
.card {
scroll-snap-align: center;
container-type: scroll-state;
container-name: card;
}
@container card scroll-state(snapped: inline) {
.card-inner {
transform: scale(1.12);
background: var(--accent);
}
}
4 · scrollable:还有内容可滚时,才显示边缘渐隐
长列表上下两端的渐隐遮罩是经典的「这里还有更多」提示。过去要算 scrollTop 决定显隐,现在 scrollable: top / scrollable: bottom 直接回答「这个方向还有没有可滚内容」——滚到顶,顶部遮罩自动消失;滚到底,底部遮罩消失:
scrollable 的 CSS 骨架:
.list {
overflow-y: auto;
container-type: scroll-state;
container-name: list;
}
.fade { position: sticky; opacity: 0; transition: opacity .2s; }
.fade.top { top: 0; }
.fade.bot { bottom: 0; }
@container list scroll-state(scrollable: top) { .fade.top { opacity: 1; } }
@container list scroll-state(scrollable: bottom) { .fade.bot { opacity: 1; } }
5 · 边界与降级
scroll-state()容器查询较新(Chrome 133+),需做渐进增强:不支持时容器查询整条不命中,元素保留默认样式——上面三个 demo 在旧浏览器里仍可正常滚动,只是少了贴顶变身 / 点亮 / 渐隐这层反馈。stuck要求容器本身是position: sticky;snapped要求容器是某个 scroll-snap 容器的 snap-area;scrollable则查询容器作为滚动容器自身的可滚余量。三者各有前提,设错前提则永不命中。- 它和 scroll-snap 的
scrollsnapchange事件互补:事件给 JS 一个回调(拿到吸附的子元素),scroll-state则纯在样式层完成,不进 JS。