CSS View Transitions

深入理解 CSS View Transitions API:如何创建跨页面和同文档状态切换的平滑过渡动画、view-transition-name 的使用、CSS 伪元素机制,以及实际应用场景。

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. 保持过渡简单

复杂的过渡动画可能导致性能问题。尽量使用 transformopacity,避免动画布局属性。

2. 使用 will-change

.animated-element {
  will-change: transform, opacity;
}

3. 避免过渡过大的元素

如果必须过渡大元素,考虑使用 ::view-transition-old::view-transition-newobject-fitobject-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 或更高优先级的选择器。


延展阅读