列表→详情页的过渡动画不用手写坐标了:view-transition-name 动态命名实战

页面里一堆卡片,点击其中一个跳到详情页,过渡动画怎么做?

以前这个需求靠两个办法:一是整个页面一起淡入淡出,毫无细节;二是自己写 JS 算位置、拼动画,代码比业务逻辑还多。

View Transitions API 解决了单页应用里的过渡,但跨文档跳转(MPA)这件事,浏览器一直没吭声。直到 pageswap 和 pagereveal 这两个事件出来,局面才彻底变了。

**这篇文章解决的问题是:怎么让「点击卡片 A」到「详情页卡片 A」的过渡动画,用几行代码接起来,而不是手写坐标计算。**

## view-transition-name 是什么

每个参与过渡动画的 DOM 元素,都有一个 view-transition-name 属性。设了同一个名字的元素,在过渡期间会被当作「同一个东西」的前后状态来处理——浏览器自动捕捉旧位置的快照,然后把它「飞」到新位置去。

“`css
/* 给卡片上的标题设一个名字 */
.card-title {
view-transition-name: product-title;
}
“`

只要详情页里对应的标题也设了同一个名字,浏览器就知道它们是「同一个东西」,自动生成从卡片位置飞到详情顶部的过渡动画。整个过程零 JS 动画代码。

## 问题是:列表里的每个卡片不能写死同一个名字

列表页一般长这样:

“`html

iPhone 16

MacBook Pro

AirPods Pro

“`

如果每个 .card-title 都写死 view-transition-name: product-title,浏览器会一脸懵——三个元素叫同一个名字,过渡的时候不知道该用哪个,动画就乱套了。

**解决方案:用 JS 动态设置,每个元素按需取唯一的名字,用完再清掉。**

## 方案一:动态绑定 view-transition-name

核心思路:在点击卡片时,给被点击的那个元素设置 view-transition-name,跳转到详情页后,详情页也对应设置同名属性。动画结束后,两个地方都清掉名字,避免影响 BFCache(后退缓存)里的页面状态。

列表页的点击逻辑:

“`js
document.querySelectorAll(‘.card’).forEach(card => {
card.addEventListener(‘click’, async (e) => {
const title = card.querySelector(‘.card-title’);
const uniqueName = `card-title-${Date.now()}`;

// 设一个临时唯一名字
title.style.viewTransitionName = uniqueName;

// 导航
const targetUrl = card.dataset.href;
await fetch(targetUrl, { navigationAPI: true });

// 动画结束后清理
// (navigationAPI 方式需要在 startViewTransition 里包装)
});
});
“`

但这个写法有个问题:导航发出后,旧页面的 view-transition-name 还在,如果用户按了后退键回到列表页,那个名字还挂着,卡片再点就会出问题。

正确做法是**等 viewTransition.finished 再清**:

“`js
document.querySelectorAll(‘.card’).forEach(card => {
card.addEventListener(‘click’, async (e) => {
e.preventDefault();
const title = card.querySelector(‘.card-title’);
const uniqueName = `card-title-${Date.now()}`;

title.style.viewTransitionName = uniqueName;

const targetUrl = card.dataset.href;

// 导航并包装进 View Transition
const transition = document.startViewTransition(() => {
window.location.href = targetUrl;
});

// 等动画做完,清掉名字
await transition.finished;
title.style.viewTransitionName = ‘none’;
});
});
“`

## 方案二:用 pageswap 事件在旧页面动态设名字

如果你不想在点击时污染 JS 逻辑,可以用 pageswap 事件,在**即将离开旧页面时**才设置名字。这样点击逻辑和动画逻辑完全解耦:

“`js
window.addEventListener(‘pageswap’, async (e) => {
if (!e.viewTransition) return;

const from = e.activation.from;
if (!from || !from.url) return;

// 找到旧页面上被点击的元素
const clickedCard = document.querySelector(‘.card:focus-within’)
|| document.querySelector(‘.card.clicked’);

if (clickedCard) {
const title = clickedCard.querySelector(‘.card-title’);
const uniqueName = `card-title-${Date.now()}`;
title.style.viewTransitionName = uniqueName;

// 存一份到 sessionStorage,详情页要用
sessionStorage.setItem(‘vtName’, uniqueName);
sessionStorage.setItem(‘vtFrom’, ‘list’);
}
});
“`

详情页(目标页)用 pagereveal 来接:

“`js
window.addEventListener(‘pagereveal’, async (e) => {
if (!e.viewTransition) return;

const vtName = sessionStorage.getItem(‘vtName’);
if (!vtName) return;

// 等 transition ready 后给详情页对应元素设上同样的名字
await e.viewTransition.ready;

const detailTitle = document.querySelector(‘.detail-title’);
if (detailTitle) {
detailTitle.style.viewTransitionName = vtName;
}

// 清理
sessionStorage.removeItem(‘vtName’);
});
“`

这套玩法的精髓在于:**名字的设置和清理都在页面生命周期里自然发生**,不需要在点击时协同两个页面的代码。

## 踩过的两个坑

**坑一:BFCache 里的名字没清干净**

如果用户按了后退键回到列表页,页面是从 BFCache 恢复的,之前设的 view-transition-name 还挂着。后续再点同一张卡片,浏览器会把两个状态都当作「同一个」来处理,动画直接抽搐。

解法:列表页初始化时主动清所有 .card-title 的 view-transition-name:

“`js
document.querySelectorAll(‘.card-title’).forEach(el => {
el.style.viewTransitionName = ‘none’;
});
“`

把这个放在 script 同步执行,不要等 DOMContentLoaded,否则 BFCache 恢复时可能会抢在清名字之前就触发动画。

**坑二:uniqueName 用 Date.now() 有小概率碰撞**

如果用户手速快,两次点击在同一个毫秒发出,名字会重复。实际场景几乎不可能发生,但如果要严谨,可以用 crypto.randomUUID():

“`js
const uniqueName = crypto.randomUUID();
“`

生成的结果类似 8a3f4c2d-1b6e-4f9a-8c7d-2e5f1a3b4c5d,碰撞概率约等于零。

## 效果预览

假设有这样的布局:

“`html

iPhone 16

列表→详情页的过渡动画不用手写坐标了:view-transition-name 动态命名实战

iPhone 16

列表→详情页的过渡动画不用手写坐标了:view-transition-name 动态命名实战

“`

对应的 CSS:

“`css
/* 列表页 */
.card-title { view-transition-name: none; }

/* 详情页 */
.detail-title {
view-transition-name: var(–vt-name);
}
.detail-img {
view-transition-name: product-image;
}
“`

注意 .detail-img 用的是固定名字 product-image,因为每张详情页只展示一个产品,不会像列表页那样有多个同名元素。

## 结论

view-transition-name 动态设置的核心逻辑就三步:

1. **点击时或 pageswap 事件里**:给旧页面被点击的元素设一个唯一名字
2. **pagereveal 事件里**:给详情页对应元素设上同样的名字
3. **动画结束后**:两边都清成 none

不需要任何动画库,不需要算坐标,浏览器原生搞定。如果你的项目还没开始用 View Transitions API,建议从详情页过渡这个场景入手——它是用户感知最明显的场景,也是这套方案最干净的实现入口。

**下一步**:把列表页和详情页的 opt-in @view-transition 加上(两个页面都需要),然后把上面的 pageswap/pagereveal 代码粘进去,跑一遍点击,看看浏览器给你「飞」出来的效果。

Chrome 126+、Safari 18+ 支持 cross-document View Transitions。如果你的用户里 Safari 占比不低,需要做个 feature detect:

“`js
if (!document.startViewTransition) {
// fallback:直接跳转,不做过渡
window.location.href = targetUrl;
return;
}
“`

评论区

0 条评论

登录后可评论。

小鹿·界面实验室 1370 阅读