使用 View Transitions API 中的兼容性问题
本站已经使用 View Transitions API 作为跨文档过渡和部分同文档过渡(主题切换)的解决方案,尽管所有代码都符合标准,但是作为2025年才逐步完善的新特性,该 API 在不同的 Chrome 版本间都存在了不同程度的问题,如果把它们没有问题的部分拼凑在一起,才能组成代码应该发挥的完整效果。
跨文档过渡
跨文档过渡只需要在网页头部自定义 CSS 和 HTML Tag 即可实现,本站已经自定义动画,使得网页间过渡更加优雅。
首先开启该特性:
@view-transition {
navigation: auto;
}
在这之后,你的网页在切换中就会出现默认的渐入渐出动画。然而截至文章发布时,Android Chrome 正式版会有一个小问题,当页面超过了 innerHeight 并且滚动到最底部的一个区域,你会发现切换中旧文档的过渡效果失效,并且会自动突然向下移动(文档向上滚动)一段距离。
这个问题同样出现在 Chrome Beta 和 Dev 版,在 Canary 版本中得到修复,但是这时出现了另一个问题,后文有述。
其次,根据 View Transitions API 的参考,你可以自定义过渡动画,过渡动画可以由“旧文档退出”和“新文档进入”两部分组成,为这两部分分别定义动画。
@keyframes move-out {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(10%);
}
}
@keyframes move-in {
from {
opacity: 0;
transform: translateY(10%);
}
to {
opacity: 1;
transform: translateY(0);
}
}
然后为 root 名称的 view-transition-name 设置 animation 属性以绑定。
::view-transition-old(root) {
animation: 0.4s move-out;
}
::view-transition-new(root) {
animation: 0.4s move-in;
}
如果不想让页面中某一元素执行该动画,为该元素设置一个 view-transition-name ,如
.header {
view-transition-name: the;
}
如果出现闪烁现象,说明过渡触发时机太早了,要定义一个元素出现后再过渡,
<link rel="expect" blocking="render" href="#added"><!-- 在文档尾部 id 为 #added 的元素 -->
同文档过渡(主题切换)
定义一个切换主题的 class,然后在点击主题切换按钮时对其增删来实现主题切换。经测试,似乎仅仅通过更改元素属性的主题切换方式无法触发 document.startViewTransition(callback) 的过渡。
const theheader = document.querySelector(".header");
const theThemeToggler = document.querySelector(".theme-toggler");
themeToggle && themeToggle.addEventListener("click", (e) => {
document.startViewTransition(() => {
theheader.classList.remove("bgh");
theThemeToggler.classList.add("ttf");
document.body.classList.toggle("light-theme");
document.body.classList.toggle("dark-theme");
}).ready.then(() => {
later(e)
});
});
function later(e) {
const {
clientX,
clientY
} = e;
const radius = Math.hypot(
Math.max(clientX, innerWidth - clientX),
Math.max(clientY, innerHeight - clientY)
);
document.documentElement.classList.add("notran");
document.documentElement.animate({
clipPath: [
`circle(0% at ${clientX}px ${clientY}px)`,
`circle(${radius}px at ${clientX}px ${clientY}px)`,
],
}, {
duration: 400,
pseudoElement: "::view-transition-new(root)",
}).finished.then(
() => {
document.startViewTransition(() => {
theheader.classList.add("bgh");
theThemeToggler.classList.remove("ttf");
}).finished.then(() => {
document.documentElement.classList.remove("notran");
});
}
);
}
在 later() 中,获取了点击事件的点击坐标,并根据其使用过渡的新视图自定义了 clip-path 圆形半径增大的动画,但是这会与跨文档过渡中已经定义的动画相冲突,所以在动画执行前为文档添加了一个类 notran,再在头部定义以下规则来禁用动画。
.notran::view-transition-new(root),
.notran::view-transition-old(root) {
animation: none;
}
由于 .header 定义了透明度颜色,而它已经禁用了 root 名称的动画,所以 clip-path 动画无法覆盖它,于是需要在动画开始前使用不透明的颜色,消除视觉上的影响,所以在动画开始前为其设置不透明的颜色,移除类 bgh,然后在在同文档过渡动画结束后加上。同样的原因,它会突然变为透明颜色,所以要调用 document.startViewTransition(callback)。由于在过渡中,主题切换按钮是不可点击的,但是视觉上的动画基本结束,所以要为这个按钮增加一点样式,表明它暂时不可点击。
.bgh {
background: var(--header)!important;
}
.ttf {
filter: opacity(0.5);
}
在所有动画结束之后移除类 notran,来恢复跨文档过渡的动画。
但是这段代码在最新的 Chrome Canary 中出现了一些问题,动画开始的坐标并不是点击的坐标,而是向左移动了一定距离。暂定为 Canary 版本的 Bug.
完整代码(头部):
<style>
@view-transition {
navigation: auto;
}
@keyframes move-out {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(10%);
}
}
@keyframes move-in {
from {
opacity: 0;
transform: translateY(10%);
}
to {
opacity: 1;
transform: translateY(0);
}
}
::view-transition-old(root) {
animation: 0.4s move-out;
}
::view-transition-new(root) {
animation: 0.4s move-in;
}
.notran::view-transition-new(root),
.notran::view-transition-old(root) {
animation: none;
}
.bgh {
background: var(--header) !important;
}
.ttf {
filter: opacity(0.5);
}
.header {
view-transition-name: the;
}
</style>
<link rel="expect" blocking="render" href="#added">
还是期待 Chrome 早点修好这两个问题吧。