侧边栏壁纸
博主头像
踏浪而行生活圈

行动起来,活在当下

  • 累计撰写 23 篇文章
  • 累计创建 7 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

两行代码,让你的网站切换丝滑得像原生 App

我一直在想一个事:为什么原生 App 的页面切换可以做得那么自然——从列表滑进详情、从缩略图展开成大图——而网页上点一个链接,永远是一刀切的白屏?

不是没有解决方案,而是那些方案都太重了

SPA 框架里写个页面过渡动画,你得:

  1. <Transition><KeepAlive> 包裹路由
  2. 写 CSS enter / leave 动画的四件套:enter-fromenter-activeenter-toleave-from……
  3. 处理新旧 DOM 共存时的 z-index、层级、事件穿透
  4. 等动画结束手动清理旧 DOM
  5. 还要操心无障碍问题——屏幕阅读器会不会同时读两页内容?

代码量一上来,维护成本直线上升。而且这套方案只对 SPA 有效,传统多页面应用(MPA)根本用不了。

但浏览器中还存在着一个全新的答案:CSS View Transitions API

丝滑过渡


什么是 View Transitions?

简单说:浏览器原生提供的页面状态过渡机制。

你在两个状态之间切换时(比如从列表页到详情页、从暗色模式到亮色模式),浏览器会自动截取"切换前"和"切换后"两张快照,然后在它们之间做动画过渡。

最基础的用法,真的就两行代码:

// 以前:直接改 DOM,画面"咔"一下
element.textContent = '新内容';

// 现在:用 startViewTransition 包一下,丝滑过渡
document.startViewTransition(() => {
  element.textContent = '新内容';
});

没了。就这一个 API 调用,浏览器就自动给你做了个渐隐渐现的过渡效果。

我当时第一次看到这段代码的时候,反复确认了三遍——就这?不用装库?不用配路由?不用写四件套?

是的,就这。


先来个完整例子,感受一下

我们先做一个最简单的例子:点击按钮,切换一张图片和文字。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>View Transitions Demo</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-family: system-ui, sans-serif;
      background: #0f0f0f;
      color: #fff;
    }
    .card {
      width: 400px;
      text-align: center;
    }
    .card img {
      width: 100%;
      border-radius: 12px;
      margin-bottom: 16px;
    }
    .card h2 {
      font-size: 1.5rem;
      margin-bottom: 8px;
    }
    .card p {
      color: #aaa;
      line-height: 1.6;
    }
    button {
      margin-top: 24px;
      padding: 10px 24px;
      border: none;
      border-radius: 8px;
      background: #fff;
      color: #000;
      font-size: 1rem;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div class="card">
    <img id="hero" src="https://picsum.photos/id/1015/400/300" alt="风景">
    <h2 id="title">山间溪流</h2>
    <p id="desc">清晨的阳光穿过树梢,溪水在石头间欢快地跳跃。</p>
  </div>
  <button onclick="switchSlide()">下一张 →</button>

  <script>
    const slides = [
      { img: 'https://picsum.photos/id/1015/400/300', title: '山间溪流', desc: '清晨的阳光穿过树梢,溪水在石头间欢快地跳跃。' },
      { img: 'https://picsum.photos/id/1018/400/300', title: '雾中湖泊', desc: '湖面像一面镜子,映着远处若隐若现的群山。' },
      { img: 'https://picsum.photos/id/1035/400/300', title: '夕阳海岸', desc: '最后一缕阳光把海面染成了橘红色,海风带着咸味。' },
    ];
    let index = 0;

    function switchSlide() {
      index = (index + 1) % slides.length;
      const slide = slides[index];

      // ⭐ 就这一行!把 DOM 更新包进 startViewTransition
      document.startViewTransition(() => {
        document.getElementById('hero').src = slide.img;
        document.getElementById('title').textContent = slide.title;
        document.getElementById('desc').textContent = slide.desc;
      });
    }
  </script>
</body>
</html>

把这段代码保存成 HTML 文件,在 Chrome 111+ 中打开,点一下"下一张"——你会看到图片和文字有一个非常自然的交叉淡入淡出效果。

如果没有 View Transitions,切换就是"咔"一下全变;有了它,就像有人在你眼前慢慢翻过一页。


让元素"独立运动":view-transition-name

默认的过渡是整个页面一起淡入淡出。但如果你想让不同的元素各自有各自的动画呢?

比如图片从左飞入、标题从下滑入、描述文字渐显——这就是 view-transition-name 登场的时候。

/* 给每个元素起一个名字 */
.hero-image {
  view-transition-name: hero;
}

.title {
  view-transition-name: title;
}

.description {
  view-transition-name: desc;
}

加上 view-transition-name 之后,浏览器会为这些元素单独生成快照,你可以对它们分别写动画:

/* 图片:从左侧滑入 */
::view-transition-group(hero) {
  animation: slide-in 0.4s ease-out;
}

/* 标题:从下方弹入 */
::view-transition-group(title) {
  animation: bounce-up 0.5s ease-out;
}

/* 描述文字:简单淡入 */
::view-transition-group(desc) {
  animation: fade-in 0.3s ease;
}

@keyframes slide-in {
  from { transform: translateX(-50px); opacity: 0; }
  to   { transform: translateX(0); opacity: 1; }
}

@keyframes bounce-up {
  from { transform: translateY(20px); opacity: 0; }
  to   { transform: translateY(0); opacity: 1; }
}

@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

效果:图片从左边飞进来,标题从下面弹上来,描述文字慢慢淡入——每个元素有自己的节奏,整个页面像一个精心编排的舞台剧。

我第一次在项目里用这个的时候,设计师看到效果直接愣住了,问我"这是原生的?不用 GSAP?不用 Framer Motion?"

对,原生的。


真正的杀手锏:跨文档过渡(MPA)

上面的例子都是 SPA——JavaScript 控制 DOM 变化。但 View Transitions 最让我兴奋的能力是:它能在多页面应用(MPA)中也能用。

什么意思?就是你从 index.html 点链接跳到 about.html纯 HTML 跳转,没有任何 JavaScript 路由,也能有丝滑的过渡动画。

实现方式极其简单——在两个页面的 CSS 里各加一行:

/* index.html 和 about.html 都加上这个 */
@view-transition {
  navigation: auto;
}

完了。浏览器就会在页面导航时自动触发过渡效果。

如果你想要更精细的控制,比如让某个元素在两个页面之间"连贯地运动"(就像 iOS 的相册点击放大效果),只需要在两个页面给同一个元素起相同的名字:

<!-- 列表页:index.html -->
<div class="thumbnail">
  <img src="photo.jpg" style="view-transition-name: photo-expand;">
</div>

<!-- 详情页:detail.html -->
<div class="fullsize">
  <img src="photo.jpg" style="view-transition-name: photo-expand;">
</div>

浏览器看到两个页面都有 view-transition-name: photo-expand 的元素,就会自动做位置和大小的补间动画——缩略图会"长"成大图,平滑地过渡过去。

这就是 iOS、Android 相册那种"点击放大"效果。以前在网页上实现这种效果,代码量是现在的 10 倍以上

页面过渡效果


深入一点:伪元素体系

View Transitions 的底层是一套伪元素体系。了解这些,你才能真正"驯服"这个 API。

::view-transition                         ← 过渡遮罩层的根
 └── ::view-transition-group(root)        ← 整个页面的过渡组
 │    ├── ::view-transition-image-pair(root)
 │    │    ├── ::view-transition-old(root)    ← 切换前的快照
 │    │    └── ::view-transition-new(root)    ← 切换后的内容
 │    └── animation: ...                      ← 默认动画
 │
 └── ::view-transition-group(hero)        ← 命名为 "hero" 的元素
      ├── ::view-transition-image-pair(hero)
      │    ├── ::view-transition-old(hero)
      │    └── ::view-transition-new(hero)
      └── animation: ...

startViewTransition 被调用时:

  1. 浏览器暂停 DOM 更新
  2. 截取当前页面的快照 → 生成 ::view-transition-old(root) 和各命名元素的 ::view-transition-old(名字)
  3. 执行 DOM 更新回调
  4. 截取更新后的快照 → 生成 ::view-transition-new(root) 和各命名元素的 ::view-transition-new(名字)
  5. 把 old 和 new 放在各自的 ::view-transition-group
  6. 执行 CSS 动画
  7. 动画结束后,移除旧快照,恢复正常渲染

明白了这个流程,你就能理解为什么有些操作在 startViewTransition 回调里做不了(比如不能异步等待 fetch 结果再更新 DOM——浏览器需要同步截取"新"状态的快照)。


进阶技巧:类型控制与条件过渡

有时候你不想所有导航都用同一种过渡效果。比如前进用滑入,后退用滑出;或者从列表到详情用展开,从详情回列表用折叠。

View Transitions Level 2 提供了**类型(type)**机制:

// 带类型的过渡
document.startViewTransition({
  update: () => {
    // 更新 DOM
    showDetailView();
  },
  types: ['forward', 'expand']  // 标记这是一次"前进+展开"
});

然后在 CSS 里根据类型应用不同的动画:

/* 前进时:从右边滑入 */
:active-view-transition-type(forward) {
  ::view-transition-old(root) {
    animation: slide-out-left 0.3s ease-in;
  }
  ::view-transition-new(root) {
    animation: slide-in-right 0.3s ease-out;
  }
}

/* 后退时:从左边滑入 */
:active-view-transition-type(backward) {
  ::view-transition-old(root) {
    animation: slide-out-right 0.3s ease-in;
  }
  ::view-transition-new(root) {
    animation: slide-in-left 0.3s ease-out;
  }
}

@keyframes slide-in-right {
  from { transform: translateX(100%); }
  to   { transform: translateX(0); }
}
@keyframes slide-out-left {
  from { transform: translateX(0); }
  to   { transform: translateX(-100%); }
}
/* ... 其他方向类似 */

这就跟 iOS 的 UINavigationController 一样的效果了——前进右滑入,后退左滑出。用 CSS 搞定,不需要任何 JavaScript 动画库。


那些"坑"和注意事项

说了这么多好处,也得聊聊实际使用中遇到的问题。

1. 异步操作的限制

startViewTransition 的回调必须是同步的。你不能在里面 await 一个 fetch 请求,因为浏览器需要立即截取新状态的快照。

如果你的数据是异步获取的,有两个思路:

// 思路一:先 fetch 再过渡
async function showDetail(id) {
  const data = await fetch(`/api/detail/${id}`).then(r => r.json());
  // 数据拿到了,再触发过渡
  document.startViewTransition(() => {
    renderDetail(data);
  });
}

// 思路二:先过渡,再加载数据(骨架屏)
function showDetail(id) {
  document.startViewTransition(() => {
    renderSkeleton(); // 先显示骨架屏
  });
  // 异步加载真实数据
  fetch(`/api/detail/${id}`).then(r => r.json()).then(data => {
    document.startViewTransition(() => {
      renderDetail(data); // 数据到了再过渡一次
    });
  });
}

思路二的体验其实很好——用户先看到一个丝滑的过渡到骨架屏,然后骨架屏再丝滑地变成真实内容。比"等数据→白屏→突然出现"要舒服得多。

2. 浏览器兼容性

截至 2026 年初:

特性ChromeEdgeFirefoxSafari
SPA View Transitions (Level 1)111+ ✅111+ ✅18+ ✅
跨文档 View Transitions (Level 2)126+ ✅126+ ✅

Firefox 目前还没有支持,Safari 在 18 版本加入了 Level 1 但 Level 2 还没有。所以目前来说:

  • SPA 场景:Chrome + Edge + Safari 可用,覆盖主流用户
  • MPA 场景:目前只有 Chromium 系浏览器

不过好消息是,View Transitions 是渐进增强的——不支持的浏览器会直接跳过过渡,页面照常工作,只是没有动画而已。所以你完全可以放心地用上,不支持的用户也不会看到白屏或者报错。

3. 性能影响

view-transition-name 会让元素被单独截图生成图层。如果给太多元素加了 view-transition-name,会导致:

  • 内存占用增加(每个命名元素一份快照)
  • 合成层增多,GPU 负担加重

经验法则:页面里同时有 view-transition-name 的元素不超过 5-8 个。多了就考虑用 root 级别的过渡,或者 view-transition-class 来分组管理。

4. match-element 的魔法

如果你不想手动给每个元素命名,View Transitions Level 2 提供了一个特殊值:

.list-item {
  view-transition-name: match-element;
}

match-element 会告诉浏览器:自动匹配新旧状态下位置和大小最接近的元素,并对它们做补间动画。

非常适合列表 → 详情的场景:列表项自动"长"成详情页的大卡片,不需要手动命名每个元素。


实战:做一个图片画廊

来一个稍微完整的例子,模拟一个图片画廊的展开效果:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>图片画廊 - View Transitions</title>
  <style>
    body {
      margin: 0;
      font-family: system-ui, sans-serif;
      background: #111;
      color: #fff;
    }

    /* 网格布局 */
    .gallery {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
      gap: 16px;
      padding: 24px;
      max-width: 1000px;
      margin: 0 auto;
    }

    .gallery img {
      width: 100%;
      border-radius: 8px;
      cursor: pointer;
      transition: transform 0.2s;
    }
    .gallery img:hover {
      transform: scale(1.03);
    }

    /* 详情遮罩 */
    .overlay {
      position: fixed;
      inset: 0;
      background: rgba(0,0,0,0.9);
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      z-index: 100;
    }
    .overlay img {
      max-width: 90vw;
      max-height: 90vh;
      border-radius: 12px;
    }

    /* ⭐ 关键:让缩略图和大图共享同一个 transition name */
    .gallery img.expandable {
      view-transition-name: photo-expand;
    }
    .overlay img {
      view-transition-name: photo-expand;
    }

    /* 自定义过渡动画 */
    ::view-transition-old(photo-expand) {
      animation: none;
    }
    ::view-transition-new(photo-expand) {
      animation: none;
    }
    /* 让浏览器自动做位置和大小的补间 */
  </style>
</head>
<body>
  <div class="gallery" id="gallery">
    <img class="expandable" src="https://picsum.photos/id/1015/200/200" onclick="expand(this)">
    <img class="expandable" src="https://picsum.photos/id/1018/200/200" onclick="expand(this)">
    <img class="expandable" src="https://picsum.photos/id/1035/200/200" onclick="expand(this)">
    <img class="expandable" src="https://picsum.photos/id/1039/200/200" onclick="expand(this)">
    <img class="expandable" src="https://picsum.photos/id/1043/200/200" onclick="expand(this)">
    <img class="expandable" src="https://picsum.photos/id/1047/200/200" onclick="expand(this)">
  </div>

  <script>
    function expand(thumb) {
      const src = thumb.src.replace('/200/200', '/800/600');

      document.startViewTransition(() => {
        // 移除缩略图上的命名(避免冲突)
        thumb.classList.remove('expandable');

        // 创建详情遮罩
        const overlay = document.createElement('div');
        overlay.className = 'overlay';
        overlay.innerHTML = `<img src="${src}">`;
        overlay.onclick = () => collapse(overlay, thumb);
        document.body.appendChild(overlay);
      });
    }

    function collapse(overlay, thumb) {
      document.startViewTransition(() => {
        overlay.remove();
        thumb.classList.add('expandable');
      });
    }
  </script>
</body>
</html>

保存成 HTML 文件在 Chrome 中打开——点击缩略图,它会平滑地"长大"成全屏大图;点击大图,又会平滑地"缩回"去变成缩略图。

整个过程不需要任何第三方库,纯原生 CSS + 几行 JavaScript。效果堪比 iOS 相册。


它对前端生态意味着什么?

聊完技术细节,我想说点"大"的。

View Transitions 的出现,意味着浏览器终于开始原生支持"应用级"的用户体验

以前我们在网页上想要流畅的过渡效果,得依赖框架(Vue Router、React Router、Next.js)+ 动画库(Framer Motion、GSAP)+ 大量样板代码。这些工具没错,但它们本质上是在弥补浏览器的能力缺失

现在浏览器直接把这些能力内置了。这对前端生态的影响可能是深远的:

  • SPA 框架的过渡方案会被部分替代:以前 Vue Router 的 <Transition> 组件是刚需,以后可能只是一个可选的封装
  • MPA 架构会重新获得关注:以前选 SPA 很大一部分原因是为了页面切换的体验,现在 MPA 也能做到,那 SPA 的"存在感溢价"就降低了
  • 设计师的话语权会变大:以前设计师说"我要一个展开动画",开发者说"实现不了"或"成本太高"。现在设计师可以更自由地提需求了,因为实现成本真的降了很多

当然,现在 Firefox 和 Safari 的支持还不够完整,短期内还不能作为唯一的方案。但方向已经很明确了——浏览器正在变成一个更好的应用平台


总结

场景以前的做法View Transitions
SPA 状态切换<Transition> + 四件套 CSSstartViewTransition(() => {...})
MPA 页面跳转❌ 没有好方案@view-transition { navigation: auto }
元素位置补间JS 计算位置 + 手写动画view-transition-name + 浏览器自动补间
动画方向控制JS 判断路由方向 + 条件动画类型机制 types: ['forward']
代码量几十到几百行几行到几十行

如果你的项目面向 Chrome/Edge 用户,现在就可以用起来了。 即使需要兼容 Firefox,也可以作为渐进增强——支持的浏览器有动画,不支持的照样能用。

不需要等所有浏览器都支持,不需要等框架更新,不需要装任何依赖。

是时候让你的网站告别那个"咔"一下的白屏了。

用户体验


参考与致谢

本文在写作过程中参考了以下资料:

🙏 特别感谢 Google Chrome 团队的 Khushal Sagar 和 Jake Archibald,他们从设计到实现推动了 View Transitions API 的落地。这个 API 的代码示例和文档质量是我见过最好的之一。


下次你写页面过渡动画的时候,先试试 View Transitions。大概率你会发现:以前写了 200 行的东西,现在 5 行就够了。

往期精彩:

腾讯推出QClaw:微信远程操控电脑,本地AI助手开启内测

小米深夜放了个"核弹":三大自研 MiMo-V2模型,这次真要"为发烧而生"

未登录也能知道你是谁?浏览器指纹了解一下!

扫码_搜索联合传播样式-标准色版-fd2n.png

0

评论区