关于 web-animation-view-transitions
用于实现浏览器 View Transitions 的实践指南,涵盖单页更新和页面导航。内容包括功能检测、共享元素、CSS 动画以及减少动态效果偏好的处理。
主要功能
- 01同文档与跨文档转场
- 02共享元素命名模式
- 03使用 CSS 定制转场动画
- 04减少动态效果与不支持浏览器的回退
- 05转场时序与 Promise 指引
适用场景
- 01为路由或视图变化添加动画
- 02让共享主视觉元素在页面间移动
- 03添加具有方向感的导航动画
- 04遵循减少动态效果偏好
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-animation-view-transitions安装后在 Claude Code 中输入/web-animation-view-transitions确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/core.mdshared-elements.mdspa.md详细介绍
View Transitions API 模式
**快速指南:**浏览器会截取旧状态的屏幕画面,让新状态保持实时,并将它们作为一棵伪元素树进行动画过渡。
document.startViewTransition()用于驱动同一文档内的变化;在两个页面上设置@view-transition { navigation: auto },则可驱动跨文档过渡。view-transition-name会将元素从根快照中分离出来,使其可以独立移动;整个动画通过 CSS 而非脚本进行自定义。
详细资源:
- examples/core.md — 功能检测、状态过渡、三个 Promise、跳过过渡、CSS 覆盖
- examples/spa.md — 主题揭示、表单步骤、标签面板、手风琴、列表重排序、减少动态效果
- examples/shared-elements.md — 主视觉动画、多元素卡片、跨文档配对、模态框
- reference.md — 命名规则、伪元素树、浏览器支持、API 表格
适用场景
- 客户端路由或页面内状态由脚本更新 —
startViewTransition()接收更新回调,并在更新前后进行捕获;请参阅 examples/spa.md。 - 服务端渲染的页面之间进行导航 — 完全无需脚本:两个文档都通过
@view-transition { navigation: auto }启用此功能,并通过pageswap和pagereveal事件设置名称;请参阅 examples/shared-elements.md。 - 某个元素需要在两个状态之间移动,而不是与其他所有元素一起交叉淡入淡出 — 该元素需要在两侧设置匹配的
view-transition-name;请参阅 examples/shared-elements.md。
<critical_requirements>
编写 View Transitions 代码前
先检查是否支持 startViewTransition,再调用;无论是否支持,都要执行更新。 API 未实现时,调用会直接缺失,而不是空操作,因此不加检查就调用会抛出异常,DOM 更改也完全不会发生。
为每个参与过渡的元素指定一个其他可见元素都没有的名称。 名称用于标识一个快照;如果两个可见元素使用同一个名称,整个过渡都会中止,而不只是它们各自的部分。
在 transition.finished 解析后,清除动态分配的 view-transition-name。 元素上的名称如果没有清除,就会与下一次为元素分配的名称冲突,因此通常是第二次导航时才出现故障,而不是第一次。
为每个自定义过渡提供 prefers-reduced-motion 分支。 页面级过渡会移动整个视口,而这正是设置该偏好的目的;应缩短持续时间,或跳过过渡并直接应用更新。
</critical_requirements>
自动检测: startViewTransition、view-transition-name、view-transition-class、@view-transition、::view-transition-old、::view-transition-new、::view-transition-group、::view-transition-image-pair、:active-view-transition-type、pageswap、pagereveal、updateCallbackDone、skipTransition、match-element
适用于:
- 状态变化幅度足够大,需要替换整个视图或其中的大块区域
- 服务端渲染网站中的页面间导航
- 一个元素在两个视图中都存在的主视觉动画
- 方向感知导航,即前进和后退需要相反的移动效果
- 元素重新排序,即元素发生移动而非变化
其他地方处理:
- 单个控件的悬停、焦点和按下反馈——没有任何内容被替换,因此不存在可捕获的前后快照对
- 速度会跨越中断延续的动效,例如在手势中途接续的弹簧动画
- 帧级时间线编排,即交付物是一条可拖动播放进度的时间线
视图过渡是一个截图与实时视图相互对照并进行动画处理。所有行为都由此而来:旧的一侧是静止像素,因此其中的任何移动都会冻结;为元素命名会将其提取为独立的快照对,并拥有自己的动画;整个树都通过 CSS 设置样式,因为它是一棵伪元素树,而不是由脚本驱动的动画。
DOM 更新本身仍然是常规操作。startViewTransition 包装的是无论如何都会发生的更改,因此对于不支持该功能的浏览器,回退方式就是直接进行更改。
<decision_framework>
元素所属的快照
Does an element exist on both sides and represent the same thing?
├─ YES -> give it one view-transition-name on both sides; it gets its own group and travels
└─ NO -> leave it in the root snapshot; it cross-fades with everything else around it
每个命名元素都会在树中增加一个组、一对图像和两个快照,因此给所有元素命名的代价大于收益——只为读者会用眼睛追随的元素命名。
默认动画还是自定义动画
Cross-fade at a different duration -> set animation-duration on ::view-transition-old/new(root)
Slide, scale, wipe -> keyframes on the old and new pseudo-elements
Direction-dependent motion -> a transition type, selected with :active-view-transition-type()
Geometry computed at the moment of the transition (a reveal from a click point)
-> await transition.ready, then animate the pseudo-element
在何处设置名称
对于页面上唯一的元素,CSS 是合适的选择,例如页眉、主视觉区域或单个面板。对于出现多次的元素,脚本才是合适的选择,因为对列表应用静态规则会让每一行都使用相同的名称,从而破坏过渡;应在过渡前为每个元素单独分配名称,并在过渡后清除,或者在支持的情况下使用 view-transition-name: match-element。
</decision_framework>
核心模式
模式 1:功能检测与回退
const SUPPORTS_VIEW_TRANSITIONS =
typeof document !== "undefined" && "startViewTransition" in document;
function updateWithTransition(updateFn: () => void | Promise<void>): void {
if (!SUPPORTS_VIEW_TRANSITIONS) {
updateFn();
return;
}
document.startViewTransition(() => updateFn());
}
在服务端渲染期间没有 document 可供检查,因此 typeof document 检查很重要。
完整代码:examples/core.md
### 模式 2:同一文档内的过渡
回调执行 DOM 更新;浏览器会在调用回调前捕获一次,并在回调完成后再捕获一次。
const transition = document.startViewTransition(async () => {
await updateFn();
});
// Options form: classify the transition for CSS to select on
const transition = document.startViewTransition({
update: () => updateDOM(),
types: ["slide-forward"],
});
await transition.finished;
| Promise | 在何时完成 |
|---|---|
transition.updateCallbackDone | DOM 更新回调执行完毕 |
transition.ready | 伪元素树已创建,动画即将开始 |
transition.finished | 动画结束,新视图已生效 |
async 回调会让页面冻结,直到回调完成,因此其中任何耗时操作都会造成明显卡顿——应先更新为加载状态,而不是等待网络往返。
完整代码:examples/core.md
模式 3:跨文档过渡
/* on both the source and the destination document */
@view-transition {
navigation: auto;
}
无需脚本,也不涉及路由器;它适用于同一源内的 push、replace 和遍历导航。只有一侧启用时,不会发生任何过渡。
完整代码:examples/shared-elements.md
模式 4:共享元素过渡
变更两侧具有匹配名称的元素会彼此过渡,而不是交叉淡入淡出。
.product-thumbnail {
view-transition-name: product-hero;
}
.product-image {
view-transition-name: product-hero;
}
::view-transition-group(product-hero) {
animation-duration: var(--hero-duration);
animation-timing-function: var(--hero-easing);
}
组会为位置和尺寸设置动画;图像对则会在组内交叉淡入淡出两个快照。如果两侧的宽高比不同,可在旧、新伪元素上设置 object-fit,以避免快照变形。
完整代码:examples/shared-elements.md
模式 5:自定义 CSS 动画
默认的交叉淡入淡出是旧、新伪元素上的 UA 样式表动画;覆盖它只需使用常规 CSS。
::view-transition-old(root) {
animation: slide-out-left var(--transition-duration) var(--transition-easing);
}
::view-transition-new(root) {
animation: slide-in-right var(--transition-duration) var(--transition-easing);
}
完整代码:examples/core.md
模式 6:方向感知过渡
过渡类型是 CSS 可据此选择样式的标签,因此前进和后退可以共用一组关键帧,只需应用不同的伪元素对。
html:active-view-transition-type(forwards) {
&::view-transition-old(content) {
animation-name: slide-out-left;
}
&::view-transition-new(content) {
animation-name: slide-in-right;
}
}
html:active-view-transition-type(backwards) {
&::view-transition-old(content) {
animation-name: slide-out-right;
}
&::view-transition-new(content) {
animation-name: slide-in-left;
}
}
document.startViewTransition({
update: () => navigateForward(),
types: ["forwards"],
});
对于只能在更新开始后才知道方向的情况,transition.types 是现有过渡上的可变集合。
完整代码:examples/spa.md
模式 7:减少动态效果
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 0.01ms !important;
}
}
const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
function shouldEnableTransitions(): boolean {
if (window.matchMedia(REDUCED_MOTION_QUERY).matches) return false;
return "startViewTransition" in document;
}
CSS 方式会缩短动画,同时保留过渡机制;对于交叉淡入淡出,这就足够了。脚本方式会完全跳过过渡,对于全视口滑动或揭示效果,这是更诚实的做法。两者之间还有第三种方式:取消 ::view-transition-group(*) 上的动画——该伪元素负责承载尺寸和位置变化——同时保留旧、新伪元素的交叉淡入淡出。这样既去掉了移动效果,又保留了反馈。
完整代码:examples/spa.md
模式 8:在过渡时计算几何信息
如果动画依赖的内容只有在点击时才知道,请等待 transition.ready,然后直接为伪元素设置动画。
const transition = document.startViewTransition(updateFn);
await transition.ready;
document.documentElement.animate(
{
clipPath: [`circle(0 at ${x}px ${y}px)`, `circle(${r}px at ${x}px ${y}px)`],
},
{
duration: REVEAL_DURATION_MS,
easing: REVEAL_EASING,
pseudoElement: "::view-transition-new(root)",
},
);
由于伪元素本身没有可供寻址的节点,因此动画会通过 pseudoElement 选项以文档元素为目标。
完整代码:examples/spa.md
<red_flags>
危险信号
运行时会出错:
- 未检查支持情况就调用
document.startViewTransition(...)— 在缺少此 API 的环境中会抛出错误,导致 DOM 更新和动画一并丢失 — 根据"startViewTransition" in document进行分支判断 - 两个可见元素使用相同的
view-transition-name— 整个过渡都会被放弃,包括原本正确的部分 — 通过脚本为每个元素分别分配名称,或使用match-element - 在列表选择器的 CSS 中设置
view-transition-name— 每一行都会声明相同的名称 — 在过渡开始前通过脚本设置,并在过渡后清除 transition.finished后仍保留已分配的名称 — 下次过渡时会发生冲突,因此故障会在原因出现后的下一次导航时才显现 — 在finished处理程序中清除名称@view-transition只写在两个文档中的一个 — 跨文档过渡需要两端都选择启用 — 也在目标文档中添加该 at-rule- 在
transition.ready解析之前就为伪元素添加动画 — 此时树尚不存在,动画会静默地作用于空对象 — 先等待该 Promise - 没有
prefers-reduced-motion分支 — 全视口滑动动画会呈现给不希望看到动画的用户 — 添加媒体查询,或在脚本中跳过过渡 <meta name="view-transition" content="same-origin">— 已过时且会被忽略 — 改用@view-transitionat-rule
容易产生意外的行为:
- 旧页面一侧是截图,因此其中的视频、GIF 和正在运行的动画都会冻结;新页面一侧则是实时画面,会继续播放
- 名称在整个文档范围内全局生效,因此即使两个无关的区域相距很远,使用相同名称也会冲突
- 过渡期间页面处于不可交互状态,因此较长的动画会直接增加交互延迟 — 导航流程中的动画应控制在 300ms 以下
- 跨文档过渡要求导航发生在同源页面之间
match-element需要 Chrome 137+ 或 Safari 18.4+,且 Firefox 不支持- 延迟注册的
pagereveal处理程序会错过该事件 — 将其放在<head>中,或为脚本添加blocking="render" auto、inherit、none和unset是 CSS 全局关键字,而非自定义标识符,因此不能用作名称