配了三年滚动交互,今天才发现「卡住没卡住、滚向哪边、能不能滚、有没有吸住」这四个状态浏览器自己会告诉你——@container scroll-state() 把这件事彻底变了

写过滚动交互的前端工程师都知道,这个坑有多深——sticky 头有没有贴上去、轮播项有没有吸住、往下滚还是往上滚、内容能不能继续滚,这些全靠 JS 监听 scroll 事件去算。写起来麻烦,跑起来还容易卡。

Chrome 133 开始,CSS 悄悄塞了一个 @container scroll-state() 查询,到 Chrome 144(2026 年 1 月)把方向检测(scrolled)也补齐了,四个状态现在全都能用纯 CSS 问了。


stuck:sticky 贴上去了吗

这个是三个状态里最实用的。以前你要给 sticky 头加个阴影,得写 IntersectionObserver 监听它有没有碰到视口顶部,现在直接在 CSS 里问就行:

.sticky-wrap {
  container-type: scroll-state;
  position: sticky;
  top: 0;
}

.header-inner {
  @container scroll-state(stuck: top) {
    background: white;
    box-shadow: 0 2px 8px rgba(0,0,0,0.15);
  }
}

stuck 支持四个方向:top/bottom/left/right,逻辑值 inset-block-start 也能用。这个在导航栏吸顶、侧边栏吸边这类场景里直接省掉一个 IntersectionObserver。


snapped:滚动吸住到哪了

给轮播图或者走马灯做「当前项高亮」,以前得在 JS 里记当前 index,切过去以后给对应卡片加 class。现在 CSS 自己知道哪个元素正在被 snap 容器锁定:

.carousel-item {
  container-type: scroll-state;
}

.card-content img {
  transition: scale 0.3s;
  scale: 0.85;

  @container scroll-state(snapped: inline) {
    scale: 1.1;
    filter: sepia(0);
  }
}

snapped 支持 inline/block/x/y 四个轴向。配合 transition-delay 可以做出「滑走时缩小、吸住时放大」的效果,全程不需要一行 JS。


scrollable:还能不能往这个方向滚

「Scroll for more」这种提示,最怕的就是内容不够长根本滚不动,你还把它显示出来。以前得算 scrollHeight 和 clientHeight,现在直接问浏览器:

.scroll-container {
  container-type: scroll-state;
}

.scroll-hint {
  opacity: 0;
  @container scroll-state(scrollable: bottom) {
    opacity: 1;
  }
  @container scroll-state(not scroll-state(scrollable: bottom)) {
    opacity: 0;
  }
}

四个方向都能测:top/bottom/left/right。这个在长列表、短内容提示、回到顶部按钮的显示/隐藏上都能用。


scrolled:用户刚才往哪滚了

这是 Chrome 144 新加的,也是最亮的一个——它能告诉你「最近一次滚动是往哪个方向」。有了这个,自动隐藏导航栏这件事终于不用 JS 了:

html {
  container-type: scroll-state;
}

.site-header {
  position: sticky;
  top: 0;
  translate: 0 0;
  transition: translate 0.3s;
}

@container scroll-state(scrolled: block-end) {
  .site-header {
    translate: 0 -100%;
  }
}

@container scroll-state(scrolled: block-start) {
  .site-header {
    translate: 0 0;
  }
}

往下滚头就藏起来,往上滚就滑回来。scrolled 的值支持物理方向(top/bottom/left/right)和逻辑方向(block-start/block-end/inline-start/inline-end),还有 none 表示还没滚过。


为什么这件事值得认真对待

不只是「少写点 JS」这么简单。scroll 事件是直接在主线程上跑的,监听一多滚动就卡。而 scroll-state() 是浏览器自己在渲染管线里算的,状态变了才通知 CSS,不存在过度触发的问题。Google 已经在把 Chromium 内部 WebUI 的老式 JS 滚动监听换成了这套 API。

另外,这套机制是容器查询模型的一个扩展,语法逻辑和 @container (max-width: 600px) 完全一样,学过容器查询的话直接上手。


现状和要注意的坑

Chrome 133 首发 stuck/snapped/scrollable 三个,Chrome 144 补了 scrolled,四状态全在 Chromium 生态里。Safari 和 Firefox 目前是 No Signal,还不在 Baseline 范围。

也就是说,现在生产环境用的话,必须用 @supports (container-type: scroll-state) 做渐进增强,不支持的浏览器走原来的 JS 方案。

另外还有两个硬性限制:

  1. 元素不能查自己——带 container-type 的元素自己不能被 scroll-state 查到,得查它的子元素。所以 DOM 结构要根据这个调整,不能像以前那样一个 div 包打天下。

  2. scrollable 问的是「能不能滚」不是「滚了多远了」——它测的是「这个方向上还有没有没显示完的内容」,不是精确位置。精细的进度条之类的还是得靠 JS。


下一步怎么用

Chrome 144 以上的用户现在已经能体验到这四个状态了。建议从最实用的 stuck 状态开始:把项目里监听 sticky 元素的 IntersectionObserver 换掉,感受一下什么叫「浏览器自己算,CSS 直接用」。

等 Safari 和 Firefox 跟进了,这套方案就会从渐进增强变成默认方案。关注 Chrome Platform Status 和 webstatus.dev 跟进进度。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 76 阅读