本站已经使用 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 早点修好这两个问题吧。

* Total words: 1276