你以为 IntersectionObserver 只能检测自身?今天 CSS 用 named timeline 把这件事彻底原生化了

每次写目录导航都要靠 IntersectionObserver 监听内容区块,再手动给目录项加 `is-active` class——这件事今天被 CSS named timeline 从根上原生化了。

## 为什么 IntersectionObserver 跨不了元素

IntersectionObserver 的原理是:观察自身与视口(或最近滚动祖先)的关系。你没法让它「盯着 A 元素看,动画发生在 B 元素上」。

但实际项目里,这种场景大量存在:

– **Sidebar 目录**:滚动的是文章容器(`.article-content`),但高亮要发生在目录项(`.toc-link`)上,两者根本不是父子关系
– **代码编辑器 + 预览窗格**:滚动编辑器时,预览区的进度条要跟着动
– **Minimap 标记**:页脚 minimap 里每个标记,需要感知对应章节是否在视口内

这在以前,要么靠 IntersectionObserver 监听内容区块、回调里找对应目录项改 class,要么直接上 GSAP ScrollTrigger。现在 CSS 原生就能做。

## Named Timeline 是什么

CSS 滚动驱动动画(scroll-driven animations)有两类 timeline:

– **`scroll()`**:绑定到某个滚动容器的滚动进度,0% = 顶部,100% = 底部
– **`view()`**:绑定到某个元素进入/离开视口的可见性进度

`scroll()` 和 `view()` 默认只能绑定到「自身或其祖先」。这对于目录高亮不够用——你的目录项不在文章容器里,是完全独立的 DOM 分支。

**named timeline** 就是来解决这个问题的:给一个元素声明一个名字,然后在任何地方通过这个名字引用它,让另一个元素的动画「跟随」那个元素的 timeline 状态。

## 两行核心代码

### 第一步:给被追踪的元素起名字

“`css
.article-section {
view-timeline-name: –section-visibility;
view-timeline-axis: block;
}
“`

`view-timeline-name` 声明了一个名为 `–section-visibility` 的 view timeline。`view-timeline-axis: block` 指定追踪垂直方向的可见性(默认就是 block,可不写)。

### 第二步:让另一个元素「跟」这个 timeline

“`css
.toc-link {
animation: highlight linear both;
animation-timeline: –section-visibility;
animation-range: contain;
}
“`

`animation-range: contain` 是关键:只有当被追踪的元素(`.article-section`)完全处于视口内时,动画才会运行。如果只是「部分可见」,动画不触发。

完整高亮动画:

“`css
@keyframes highlight {
from { opacity: 0.5; color: inherit; }
to { opacity: 1; color: #3b82f6; }
}
“`

目录项会在对应章节完全进入视口时自动高亮,不需要任何 JS。

## 三个真实场景

### 场景一:Sidebar 目录自动高亮

“`html

“`

“`css
.article-section {
view-timeline-name: –section;
/* 每个 section 用 data 属性或 id 区分名字 */
}

/* 每个目录项绑定到对应 section 的 timeline */
.toc-link[data-section=”1″] { animation-timeline: –section-1; }
.toc-link[data-section=”2″] { animation-timeline: –section-2; }
.toc-link[data-section=”3″] { animation-timeline: –section-3; }

@keyframes toc-highlight {
from { background: transparent; }
to { background: #dbeafe; }
}

.toc-link {
animation: toc-highlight linear both;
animation-range: contain;
/* contain = section 完全进入视口才高亮 */
}
“`

### 场景二:编辑器进度条驱动预览窗格

编辑器滚动时,旁边的预览窗格顶部进度条跟着动:

“`css
.code-editor {
overflow-y: auto;
scroll-timeline-name: –editor-scroll;
}

.preview-progress-bar {
position: sticky;
top: 0;
height: 3px;
background: #3b82f6;
transform-origin: left;
animation: grow linear both;
animation-timeline: –editor-scroll;
animation-fill-mode: both;
}

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

没有一行 JS,进度条和编辑器滚动完美同步。

### 场景三:Minimap 多区块标记

“`css
.section-a { view-timeline-name: –section-a; }
.section-b { view-timeline-name: –section-b; }
.section-c { view-timeline-name: –section-c; }

.minimap-marker-a {
animation: pulse linear both;
animation-timeline: –section-a;
animation-range: contain;
}
.minimap-marker-b {
animation: pulse linear both;
animation-timeline: –section-b;
animation-range: contain;
}
/* 依此类推 */
@keyframes pulse {
0%, 100% { opacity: 0.4; transform: scale(1); }
50% { opacity: 1; transform: scale(1.2); }
}
“`

每个标记只在自己对应的章节完全可见时放大脉冲,完全跨 DOM。

## 浏览器支持与渐进增强

Chrome 115+、Edge 115+、Safari 26.4+、Firefox 132+(2024年10月稳定),全球覆盖率约 **84-90%**。

Firefox 目前是唯一需要确认的——它在 132 稳定版已实现,但建议生产环境仍用 @supports 做渐进增强:

“`css
.toc-link {
/* 不支持时的降级状态 */
opacity: 0.6;
transition: opacity 0.2s;
}

@supports (animation-timeline: view()) {
.toc-link {
opacity: 1;
animation: highlight linear both;
animation-timeline: var(–section-timeline);
animation-range: contain;
}
}
“`

另外,`@media (prefers-reduced-motion: reduce)` 里把 animation 禁用,保证有运动敏感需求的用户不受影响。

## 三个坑

**一、Firefox 仍要确认**
Firefox 132+ 已稳定,但旧版本不支持。生产环境必须用 @supports 渐进增强,否则目录项在 Firefox 里完全不高亮。

**二、名字冲突**
同一个页面给多个 section 用相同的 `–section-visibility` 名字,所有引用会指向同一个 timeline。正确做法:

“`css
#intro { view-timeline-name: –intro; }
#features { view-timeline-name: –features; }
#pricing { view-timeline-name: –pricing; }
“`

**三、`animation-fill-mode: backwards` 是关键**
配合 `animation-range: contain` 时,必须写 `animation-fill-mode: backwards`(或写在 `animation` 简写里)。否则在动画范围外,`@keyframes` 的 `from` 状态不会提前应用,元素会在 timeline 生效的瞬间「跳」入第一帧。

“`css
.toc-link {
animation: highlight linear backwards;
animation-timeline: –section-visibility;
animation-range: contain 0% contain 100%;
}
“`

## 下一步

下次写目录导航或滚动同步场景前,先问自己三个问题:

1. **有没有不在滚动容器内的元素需要跟随滚动状态?** 有就用 named timeline
2. **滚动行为和动画目标是否跨 DOM 分支?** 是就用 named view timeline
3. **是否需要滚动进度精确映射(如编辑器→进度条)?** 是就用 named scroll timeline

然后:找自己项目里最常用的一个场景(目录高亮或进度条),把 IntersectionObserver 代码删掉,用上面的方案替换。你会发现整个交互逻辑从 20 行 JS 变成 5 行 CSS。

**相关资源**

– [Josh W. Comeau: Scroll-Driven Animations](https://www.joshwcomeau.com/animation/scroll-driven-animations)(named timeline 实战,TOC/编辑器/minimap 三个真实示例)
– [Norman Meyer: 6 Patterns I Ship in 2026](https://www.normanmeyer.de/blogs/lab/css-scroll-driven-animations-6-patterns-i-ship-in-2026)(生产验证过的 6 种场景)
– [CSSAwwwards Complete Guide 2026](https://cssawwwards.com/blog/css-scroll-driven-animations-guide-2026)(animation-range 所有关键词详解)
– [Can I Use: scroll-driven animations](https://caniuse.com/css-animation-timeline)(全球覆盖率实时数据)

评论区

0 条评论

登录后可评论。