我如何在博客中使用CSS View Transitions
摘要
作者详细介绍了如何在静态博客上仅使用CSS跨文档视图过渡来为页面导航添加动画,无需JavaScript路由,包括设置布局列和动画。
<p><a href="https://lobste.rs/s/hxjscx/how_i_m_using_css_view_transitions_on_this">评论</a></p>
查看缓存全文
缓存时间: 2026/07/07 16:18
# 我是如何在这个博客中使用 CSS View Transitions 的
来源:https://blog.omgmog.net/post/how-im-using-css-view-transitions-on-this-blog/
在网页的旧时代,页面间的动画效果需要借助 jQuery 来模拟。接着出现了[pjax](https://github.com/defunkt/jquery-pjax),它利用 `pushState` 替换获取的内容,伪造出没有真正导航的全页切换,同时仍需依赖 `popState` 来防止返回按钮失效。此后几乎所有的单页应用(SPA)都在做同样的事:拦截链接点击、获取新内容、替换内容、自行处理切换动画。这种方式虽然可行,但每个导航都需要经过 JS 路由,即使是原本无需路由的静态博客也不例外。
[跨文档视图过渡](https://developer.mozilla.org/en-US/docs/Web/CSS/@view-transition) 无需借助任何 JavaScript API 就能实现相同的效果。只需在 CSS 中添加一条 `@view-transition` 规则即可。不需要路由、不需要 fetch、也不需要客户端历史劫持——浏览器仍然会执行一次真实的导航,访问真实的 URL,只是在离开页面时捕获旧页面,并让 CSS 将其与传入的新页面进行动画过渡。
另外还有 [JavaScript View Transition API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API)(`document.startViewTransition()`),它用于在 SPA 内部进行同文档过渡。这是针对不同场景的不同工具——这个博客没有需要接入的路由客户端,因此下文所有内容都是纯 CSS 的版本。
真正让我开始认真研究这个话题的是 [Jim Nielsen 的博客](https://blog.jim-nielsen.com/),点击一篇文章标题时,它会从列表直接“变形”为文章页面的 `<h1>` 标题。这是一种不错的润色效果,且运行时不消耗任何性能,于是我深入探究了它的工作原理。
## 开启它只需一条声明
所有功能只需在一个 `@view-transition` 块内加上 `navigation: auto`,并包裹在 `prefers-reduced-motion` 检查中,这样对于请求减少动画的用户,它就会保持关闭:
```css
@media (prefers-reduced-motion: no-preference) {
@view-transition {
navigation: auto;
}
}
```
现在,所有同源导航都会以交叉淡入淡出的方式代替硬切换。Chrome、Edge 和 Safari 自 2024 年中起已全面支持([caniuse](https://caniuse.com/mdn-css_at-rules_view-transition)),Firefox 则会忽略该块并按正常方式导航,因此无需进行特性检测。
那个默认的交叉淡入淡出效果应用于整个页面作为一个整体,这虽然可行但不够有趣,也不是我所期望的栏目滑动效果。所以我将其从根元素上去除:
```css
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
```
通过给单个元素命名为 `view-transition-name`,可以让页面的特定部分自行独立动画。
## 实现整个布局的过渡
我的布局使用了一个包含三个区域的网格(`sidebar-left`、`page-content`、`sidebar-right`),因此我直接命名这些栏目本身,而不是其内部的元素,并给中间栏目添加了一个滑动动画:离开时向左滑出,进入时从左侧滑入:
```css
.sidebar-left {
view-transition-name: first-column;
}
.page-content {
view-transition-name: middle-column;
}
@keyframes slide-left-out {
from {
transform: translateX(0) scaleX(1);
opacity: 1;
}
to {
transform: translateX(-50vw) scaleX(0);
overflow-x: hidden;
opacity: 0;
}
}
@keyframes slide-left-in {
from {
transform: translateX(-50vw) scaleX(0);
overflow-x: hidden;
opacity: 0;
}
to {
transform: translateX(0) scaleX(1);
opacity: 1;
}
}
@media (min-width: 768px) {
::view-transition-old(middle-column) {
animation: slide-left-out 0.1s forwards;
}
::view-transition-new(middle-column) {
animation: slide-left-in 0.2s forwards;
mix-blend-mode: normal;
}
}
```
离开动画的时间比进入更短(0.1s 对 0.2s),这给人的感觉是旧页面快速让开,而不是对称的交换。`mix-blend-mode: normal` 也很重要——浏览器的默认混合模式会导致实色内容在滑动过程中被冲刷掉,而 `normal` 可以修复这个问题。
另外,两个侧边栏需要在 `::view-transition-group` 中显式设置 `z-index`,否则滑动的中间栏会覆盖侧边栏而不是从下方穿过:
```css
::view-transition-group(first-column) {
z-index: 2;
}
::view-transition-group(middle-column) {
z-index: 1;
}
```
侧边栏本身不滑动,但其内部的小组件(标签列表、webmentions、相关文章)在不同页面间会发生变化,因此每个小组件通过自定义属性获得各自的过渡名称:
```css
.vt {
view-transition-name: var(--vtn);
animation-duration: 0.4s;
animation-timing-function: linear;
}
.sidebar-webmentions {
--vtn: sidebar-webmentions;
}
.sidebar-tags {
--vtn: sidebar-tags;
}
.sidebar-related-posts {
--vtn: sidebar-related-posts;
}
```
然后在 include 中只需添加 `class="sidebar-tags vt"`。每个小组件会在自己的时间线上交叉淡入到新内容,而不是整个侧边栏块同时重新布局。因此当内容高度变化时(例如一篇有三标签的文章与一篇无标签的文章),也不会拖拽页面其他部分。
## 内容错位闪光
这里有一个陷阱。浏览器会在导航前后捕获每个命名元素的大小和位置,如果这个盒子不稳定,动画会从一个错误的位置开始,出现一帧可见的闪光,然后才自行纠正。有两个元凶。
作者介绍块中的照片已经设置了 `max-width` 和 `aspect-ratio`,但缺少固定的 `width`,因此 `aspect-ratio` 无法计算出高度,导致盒子在图片加载前塌缩:
```css
.author-photo {
max-width: 100%;
width: 25rem;
aspect-ratio: 1;
}
```
加上 `width: 25rem` 就解决了问题——尽管大多数布局中 `max-width: 100%` 会覆盖它,但 `aspect-ratio` 需要一个明确的宽度来推导高度,`width` 提供了这个宽度,`max-width` 只是随后进行上限约束。
中间栏目也有类似的问题,但原因不同。它的网格轨道只是 `5fr`,一个纯粹的比例,没有下限:
```css
--main-template-columns: 1fr 5fr 2fr;
```
纯粹的 `fr` 轨道会根据另外两个轨道缩放或收缩,因此任何改变侧边栏大小(哪怕是很小的变化)都会同时影响中间栏的宽度。不同页面下侧边栏内容不同,会导致中间栏宽度相差几个像素,过渡会从一个宽度开始动画到另一个宽度,产生可见的跳跃而非干净的滑动。
给它一个下限也能解决这个问题:
```css
--main-template-columns: 1fr minmax(50%, 5fr) 2fr;
```
`minmax(50%, 5fr)` 在低于该下限时保持相同的弹性比例,但防止了当侧边栏变化时中间栏固定在略有不同的宽度上。
还有一个值得注意的陷阱,尽管我还没遇到过:跨文档过渡有一个静默的四秒超时。如果新页面在此时间内尚未渲染,过渡就会静默失败,没有错误提示也没有控制台警告。这里无需修复,只是在连接较慢时如果过渡神秘失效,需要考虑到这一点。
以上所有内容都不需要回退。不支持 `@view-transition` 的浏览器会跳过该块并按正常方式导航(没有交叉淡入、没有滑动、只是普通的页面切换)。没有东西会损坏,也不需要 `@supports` 检查。从另一个方向看,减少动画也是如此。整个功能位于 `prefers-reduced-motion: no-preference` 后面,因此任何在操作系统中要求减少动画的用户也会获得普通的导航。我不需要为滑动效果编写一个“减少”版本,根本不存在。
我喜欢 `@view-transition` 的原因在于它是附加的——它叠加在已经正常工作的导航之上,而不是用一个路由器替换浏览器原生行为。如果 CSS 写错了或者完全跳过它,最坏的结果也就是一次无聊的页面切换,这正是所有人之前就已经拥有的体验。
## 延伸阅读
- [初探视图过渡](https://css-tricks.com/toe-dipping-into-view-transitions/) —— 一个温和的入门教程,演示了在页面间给文章标题添加动画,并遇到了命名冲突问题。
- [跨文档视图过渡,第一部分](https://css-tricks.com/cross-document-view-transitions-part-1/) —— 正确的 `@view-transition` 语法,以及对超时和图片压缩陷阱的详尽说明。
- [跨文档视图过渡,第二部分](https://css-tricks.com/cross-document-view-transitions-part-2/) —— 使用 `view-transition-class` 进行规模化命名,并通过 `pageswap`/`pagereveal` 事件即时分配名称,而非固定在每个页面中。
- [视图过渡的一些实用示例,提升你的 UI](https://piccalil.li/blog/some-practical-examples-of-view-transitions-to-elevate-your-ui/) —— 真实的 UI 模式:星球大战风格的划屏、方向性步骤流程、排序或过滤时自动命名表格行。
- [CSS View Transition API 实用指南](https://cydstumpel.nl/a-practical-guide-to-the-css-view-transition-api/) —— 更多关于宽高比陷阱的内容,以及一个有用的提示:`clip-path` 在过渡伪元素上会被忽略。
相似文章
跨文档视图转换:无人提及的那些陷阱
一篇技术文章,解释CSS中跨文档视图转换的当前实现,涵盖已弃用的meta标签、常见陷阱(如4秒超时和宽高比变形)以及正确的选择加入方法。
使用 Temporal 和 CSS color-mix 实现基于时间的背景颜色过渡
本文演示了如何使用 Temporal API 检测本地时间,并结合 CSS color-mix 在个人网站上实现根据一天中的时间变化背景颜色过渡的效果。
CSS原生视差效果
使用滚动驱动动画时间线的CSS原生视差效果,提供了一个高性能且简单的工具类,无需JavaScript。
Tw-fade: 纯CSS滚动驱动的边缘遮罩
Tw-fade 是一个新的 Tailwind CSS 插件,用于纯 CSS 滚动驱动的边缘淡出效果,无需 JavaScript 即可实现优雅的淡出效果。
CSS:不可避免的坏部分
一位非Web开发者的个人博客文章讨论了CSS中不可避免的坏部分,包括布局难题、浏览器默认设置和过度使用包装器,同时强调了可处理简单任务的子集。