写滚动动画我被「嵌套关系」卡了三年,今天 CSS 自己把这件事解开了——timeline-scope 把滚动动画从 DOM 树里彻底解放了

配滚动动画配了三年,每次想用滚动位置驱动一个动画,第一件事就是检查这个元素有没有一个能滚动的父级——没有的话,要么给它套一层 div,要么改 DOM 结构,要么直接放弃。scroll() 这个 API 我早就知道用,但每次一碰到「要驱动的元素」和「滚动容器」不是父子关系,就卡住了。直到今天我把 timeline-scope 这个属性彻底搞明白,才发现这个问题从根上就被 CSS 自己解了。

为什么 scroll() 会被 DOM 关系卡住

先说以前的问题。写 animation-timeline: scroll() 的时候,默认值是 scroll(nearest),浏览器从当前元素往上找最近的滚动祖先,然后拿它的滚动位置做时间线。这套逻辑本身没问题,但有个隐含前提:动画元素必须是滚动容器的后代。如果你要动画的元素是兄弟节点、是 header、是 fixed 定位的元素,甚至是页面另一侧的 sidebar 进度条——scroll(nearest) 全都会失效,而且没有任何报错,动画就是不动。

比如这个场景:侧边栏有个进度条,要跟着主内容区域的滚动走。但侧边栏和主区域是平级的兄弟节点,scroll(nearest) 找到的是自己的容器,根本不是主区域。这个坑我踩过不止一次,最后都是靠 JS 算滚动比例填上去的。

timeline-scope 的作用:把命名时间线的作用域撑开

解法分三步走,核心就在 timeline-scope 这个属性。

第一步,给滚动容器起个名字。

.content-area {
  overflow-y: scroll;
  scroll-timeline-name: --main-scroll;
}

第二步,在两者的共同祖先上,用 timeline-scope 把这个名字声明出来,作用范围就从滚动容器扩散到了整个祖先元素。

.layout-wrapper {
  timeline-scope: --main-scroll;
}

第三步,在任何子元素上直接引用这个名字。

.sidebar-progress {
  animation: grow linear;
  animation-timeline: --main-scroll;
}

@keyframes grow {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

这三段 CSS 一拼,侧边栏进度条就会跟着主内容区域的滚动走了——两个元素之间不需要任何 DOM 嵌套关系。timeline-scope 的本质就是:把一个定义在后代元素上的命名时间线,向上提升作用域,让祖先节点以及祖先的所有其他后代都能访问到这条时间线。

@starting-style 把入场动画也收了

说完滚动时间线,再提一个实际开发中经常碰到的配套问题:列表项入场的时候要有个淡入动画。以前写是给每个 item 加延迟,手动错开。

现在用 @starting-style:

.list-item {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.4s ease, transform 0.4s ease;
}

@starting-style {
  .list-item {
    opacity: 0;
    transform: translateY(1rem);
  }
}

@starting-style 的作用是:给浏览器一个「第一个渲染帧」之前的假想状态,让过渡动画从那个状态开始跑,而不是直接跳到最终样式。新插入的 DOM 元素、display:none 变成可见、popover/dialog 打开——这些第一次出现的时刻,以前 CSS 过渡都抓不住,@starting-style 把它填上了。

两个东西配合起来能做什么

一个典型场景:页面侧边目录跟随主内容滚动,同时每个章节标题有入场动画。目录的进度条用 timeline-scope 挂到主内容区的滚动时间线上,章节标题用 @starting-style 做入场,完全不需要一行 JS。

.page-layout {
  timeline-scope: --doc-scroll;
}

.main-scroll {
  overflow-y: scroll;
  scroll-timeline-name: --doc-scroll;
}

.toc-item {
  animation: toc-grow linear;
  animation-timeline: --doc-scroll;
}

.chapter-title {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

@starting-style {
  .chapter-title {
    opacity: 0;
    transform: translateY(1.5rem);
  }
}

浏览器支持情况

Chrome 115+ 和 Edge 115+ 已经完整支持 scroll-timeline-name、timeline-scope 和 @starting-style。Safari 18+ 支持 @starting-style,scroll-timeline 还在推进中。Firefox 目前需要手动开启配置项。生产环境用的话建议加一下 @supports 渐进增强:

@supports (animation-timeline: --test) {
  .sidebar-progress {
    animation: grow linear;
    animation-timeline: --main-scroll;
  }
}

不支持的浏览器里,动画元素直接以最终状态渲染,不会有任何错误,只是没有动画效果——这个降级策略是安全的。

下一步可以做什么

把页面上任意一个需要跟着滚动走的进度指示器、阅读比例条、目录高亮,从 JS 改成 timeline-scope + scroll-timeline-name,代码行数基本能从二十行压到五行以内。这是 2026 年 CSS 最值得迁掉的技术债之一。

评论区

0 条评论

登录后可评论。

小鹿·界面实验室 342 阅读