CSS View Transitions
为什么 View Transitions 是 Web 动画的里程碑
在 Web 应用中,页面之间的切换往往是生硬的——内容突然消失,新内容突然出现。这种体验与原生应用流畅的过渡动画形成了鲜明对比。View Transitions API 允许开发者创建跨页面或同文档状态切换的平滑过渡动画,为 Web 用户体验带来质的飞跃。
传统上,要实现页面过渡动画,开发者需要依赖 SPA 框架的路由系统或复杂的 JavaScript 动画库。View Transitions API 将这些能力原生内置到浏览器中,使得纯 CSS(或最小 JavaScript)实现流畅页面过渡成为可能。
理解 View Transitions API 的工作原理,对于构建具有原生应用般流畅体验的 Web 应用至关重要。
面试定位:View Transitions 是相对较新的 CSS API(2023+),不是面试高频考点,但它代表了 Web 动画的未来方向。面试官通过候选人对 API 设计理念的理解、对伪元素机制的了解、以及实际应用场景的探索,来评估其技术视野和工程判断能力。
View Transitions 解决什么问题
在传统的 Web 页面导航中,用户点击链接后,浏览器立即卸载旧页面,加载新页面。这种"硬切"体验在用户阅读长内容或需要上下文导航时特别令人困惑。
View Transitions API 通过在页面变化时创建快照过渡动画来解决这个问题:
传统方式:
旧页面 → 立即消失 → 新页面(无过渡)
View Transitions 方式:
旧页面 → 淡出/滑出动画 → 新页面淡入/滑入动画
这种过渡动画帮助用户维持空间认知,理解他们从哪里来、要到哪里去。
基本用法
启用 View Transitions
View Transitions 可以通过 JavaScript 或纯 CSS 方式启用。
JavaScript 方式:startViewTransition()
// 基本的 View Transition
document.startViewTransition(() => {
// 在这个回调中更新 DOM
updateDOM();
});
startViewTransition() 接受一个回调函数,在回调中执行 DOM 更新。浏览器会自动捕获更新前后的状态,并创建过渡动画。
返回值:ViewTransition 对象
const transition = document.startViewTransition(() => {
updateDOM();
});
// transition 提供了几个事件
transition.ready.then(() => {
// 过渡动画开始播放
});
transition.finished.then(() => {
// 过渡动画完成
});
transition.updateCallbackDone.then(() => {
// DOM 更新完成
});
CSS 方式:@view-transition 规则
对于同文档内的状态切换,可以使用 @view-transition CSS 规则:
/* 启用同文档的 View Transitions */
@view-transition {
navigation: auto;
}
/* 自定义过渡时长 */
@view-transition {
animation-duration: 300ms;
}
view-transition-name 属性
view-transition-name 是 View Transitions API 的核心 CSS 属性。它为元素指定一个过渡名称,使得浏览器能够将该元素的旧状态和新状态关联起来进行动画。
基本语法
.element {
view-transition-name: header; /* 命名过渡元素 */
}
当元素的 view-transition-name 相同时,浏览器会将它们视为同一个逻辑元素的"之前"和"之后"状态。
使用场景
/* 为页面头部添加过渡名称 */
.page-header {
view-transition-name: header;
}
/* 为图片添加过渡名称,实现图片放大效果 */
.hero-image {
view-transition-name: hero-image;
}
/* 关闭特定元素的过渡 */
.no-transition {
view-transition-name: none;
}
View Transition 伪元素
View Transitions API 定义了一组伪元素,用于自定义过渡动画的外观。这些伪元素形成了一个层叠结构:
::view-transition
└── ::view-transition-group(name)
├── ::view-transition-image-pair(name)
│ ├── ::view-transition-old(name)
│ └── ::view-transition-new(name)
::view-transition
根伪元素,容器覆盖整个视口。所有过渡动画都在这个容器内进行。
::view-transition {
position: fixed;
inset: 0;
z-index: 2147483647; /* 最大 z-index */
}
::view-transition-group(name)
单个过渡的根元素。当元素在动画过程中尺寸或位置发生变化时,这个组会处理这些变化。
::view-transition-group(header) {
animation-duration: 300ms;
/* 可以添加自定义缓动 */
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
::view-transition-image-pair(name)
包含旧状态快照和新状态的容器。
::view-transition-image-pair(header) {
isolation: auto;
}
::view-transition-old(name) 和 ::view-transition-new(name)
分别代表过渡前的快照和过渡后的实时表示。
::view-transition-old(header) {
animation: 300ms ease-out fade-out;
}
::view-transition-new(header) {
animation: 300ms ease-in fade-in;
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
}
实战:创建平滑页面过渡
示例一:滑动页面过渡
常见的页面过渡效果是新页面从右侧滑入,同时旧页面滑出。
/* 1. 为页面内容添加过渡名称 */
.page-header {
view-transition-name: header;
}
.page-content {
view-transition-name: content;
}
.page-sidebar {
view-transition-name: sidebar;
}
/* 2. 自定义过渡动画 */
::view-transition-old(content) {
animation: slide-out 300ms ease-out forwards;
}
::view-transition-new(content) {
animation: slide-in 300ms ease-in forwards;
}
@keyframes slide-out {
to {
transform: translateX(-30%);
opacity: 0;
}
}
@keyframes slide-in {
from {
transform: translateX(30%);
opacity: 0;
}
}
示例二:共享元素过渡
当用户点击一张图片时,该图片放大到全屏显示。这就是"共享元素过渡"——同一个元素在两个视图之间平滑过渡。
/* 在列表页面 */
.gallery-image {
view-transition-name: gallery-item;
}
/* 在详情页面 */
.full-image {
view-transition-name: gallery-item;
}
/* 确保过渡时保持元素位置 */
::view-transition-group(gallery-item) {
animation-duration: 400ms;
/* 默认会处理位置和尺寸变化 */
}
示例三:渐变过渡
最简单的过渡效果是淡入淡出。
::view-transition-old(root) {
animation: 200ms ease-out fade-out;
}
::view-transition-new(root) {
animation: 200ms ease-in fade-in;
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
}
示例四:缩放过渡
元素在过渡时可以伴随缩放效果。
.card {
view-transition-name: card;
}
::view-transition-old(card) {
animation: 350ms ease-out scale-down;
}
::view-transition-new(card) {
animation: 350ms ease-in scale-up;
}
@keyframes scale-down {
to {
transform: scale(0.9);
opacity: 0;
}
}
@keyframes scale-up {
from {
transform: scale(1.1);
opacity: 0;
}
}
跨文档过渡(MPA)
View Transitions API 还支持跨文档(Multi-Page App)过渡,这是传统 MPA 网站也能受益的特性。
启用跨文档过渡
在目标文档的 <head> 中添加:
<link rel="expect" href="/next-page" as="document">
或者使用 CSS:
@view-transition {
navigation: auto;
}
cross-origin 限制
出于安全考虑,跨文档过渡只允许在同一 origin 内进行。
JavaScript 控制
跳过过渡
// 跳过后续的过渡动画
document.startViewTransition(() => {
updateDOM();
}, { updateCallback: () => {
// 可以在这里取消过渡
return false; // 返回 false 会跳过过渡
}});
监听过渡状态
const transition = document.startViewTransition(() => {
updateDOM();
});
transition.ready.then(() => {
console.log('动画开始播放');
// 可以在这里修改伪元素的样式
document.styleSheets[0].insertRule(`
::view-transition-group(header) {
animation-timing-function: ease-in-out;
}
`);
});
transition.finished.then(() => {
console.log('动画完成');
});
操作伪元素样式
// 在动画开始前修改伪元素
transition.ready.then(() => {
const style = document.createElement('style');
style.textContent = `
::view-transition-group(large-image) {
animation-duration: 500ms;
}
`;
document.head.appendChild(style);
});
性能优化建议
1. 保持过渡简单
复杂的过渡动画可能导致性能问题。尽量使用 transform 和 opacity,避免动画布局属性。
2. 使用 will-change
.animated-element {
will-change: transform, opacity;
}
3. 避免过渡过大的元素
如果必须过渡大元素,考虑使用 ::view-transition-old 和 ::view-transition-new 的 object-fit 和 object-position 属性。
4. 合理设置 duration
过长的过渡动画会让用户感到拖沓。通常 200-400ms 是比较舒适的区间。
浏览器支持与注意事项
当前支持状态
View Transitions API 在 Chromium 内核浏览器(Chrome 111+、Edge 111+)中得到支持。Firefox 和 Safari 尚未完全实现。
使用 Feature Query 进行渐进增强:
@supports (view-transition-name: header) {
.page-header {
view-transition-name: header;
}
}
常见问题
1. 元素没有过渡名称但仍然有过渡
浏览器会为没有 view-transition-name 的元素创建默认的"root"过渡。可以使用以下方式关闭:
::view-transition-old(root) {
animation: none;
}
::view-transition-new(root) {
animation: none;
}
2. 过渡动画不流畅
检查是否在 startViewTransition 回调中进行了耗时操作。DOM 更新应该在回调中同步完成,异步操作应该提前完成。
3. 伪元素样式不生效
确保 CSS 选择器的优先级足够高。伪元素的选择器可能需要使用 !important 或更高优先级的选择器。