路由过渡动画与视图过渡
页面切换不应该是"闪白"——流畅的过渡动画让用户感知到空间关系、降低认知负担、提升产品品质感。Nuxt4 内置了完善的 CSS Transition 支持,更集成了浏览器原生的 View Transitions API,让跨页面的共享元素动画成为可能。本章从基础 CSS 过渡讲到原生 View Transitions,最终实现 AI 视频详情页的沉浸式转场效果。
1. NuxtPage 的 transition prop
1.1 全局页面过渡
在 nuxt.config.ts 中配置全局默认过渡动画:
// nuxt.config.ts
export default defineNuxtConfig({
app: {
pageTransition: {
name: 'page',
mode: 'out-in', // 旧页面先离开,新页面再进入
},
},
})对应的 CSS(在全局样式文件中定义):
/* app/assets/css/transitions.css */
/* 淡入淡出 */
.page-enter-active,
.page-leave-active {
transition: opacity 0.3s ease;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
}1.2 过渡模式
| 模式 | 行为 | 适用场景 |
|---|---|---|
out-in | 旧页面离开完毕后,新页面进入 | 最常用,避免布局闪烁 |
in-out | 新页面先进入,旧页面再离开 | 特殊效果(如覆盖动画) |
| 默认(无 mode) | 新旧页面同时过渡 | 滑动类动画 |
1.3 NuxtPage 上直接指定
<!-- app/app.vue -->
<template>
<NuxtLayout>
<NuxtPage
:transition="{
name: 'slide',
mode: 'out-in',
}"
/>
</NuxtLayout>
</template>/* 左滑入场 */
.slide-enter-active,
.slide-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.slide-enter-from {
transform: translateX(30px);
opacity: 0;
}
.slide-leave-to {
transform: translateX(-30px);
opacity: 0;
}1.4 布局过渡
切换布局时也可以应用过渡动画:
// nuxt.config.ts
export default defineNuxtConfig({
app: {
layoutTransition: {
name: 'layout',
mode: 'out-in',
},
},
}).layout-enter-active,
.layout-leave-active {
transition: opacity 0.2s ease;
}
.layout-enter-from,
.layout-leave-to {
opacity: 0;
}2. 基于路由元数据的差异化过渡
2.1 不同页面使用不同过渡
通过 definePageMeta 为每个页面指定不同的过渡效果:
<!-- app/pages/index.vue — 首页:淡入淡出 -->
<script setup lang="ts">
definePageMeta({
pageTransition: { name: 'fade', mode: 'out-in' },
})
</script>
<!-- app/pages/videos/[id].vue — 详情页:从右滑入 -->
<script setup lang="ts">
definePageMeta({
pageTransition: { name: 'slide-right', mode: 'out-in' },
})
</script>
<!-- app/pages/(auth)/login.vue — 登录页:从底部弹入 -->
<script setup lang="ts">
definePageMeta({
pageTransition: { name: 'slide-up', mode: 'out-in' },
})
</script>对应 CSS:
/* 淡入 */
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
/* 右滑 */
.slide-right-enter-active, .slide-right-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.slide-right-enter-from { transform: translateX(50px); opacity: 0; }
.slide-right-leave-to { transform: translateX(-50px); opacity: 0; }
/* 底部弹入 */
.slide-up-enter-active, .slide-up-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.slide-up-enter-from { transform: translateY(30px); opacity: 0; }
.slide-up-leave-to { transform: translateY(-30px); opacity: 0; }2.2 禁用特定页面的过渡
<script setup lang="ts">
definePageMeta({
pageTransition: false, // 该页面不使用过渡动画
})
</script>2.3 动态过渡(根据导航方向)
根据前进/后退使用不同的过渡方向:
<!-- app/app.vue -->
<script setup lang="ts">
const route = useRoute()
const transitionName = ref('slide-left')
// 监听路由变化,判断方向
const routeDepth = (path: string) => path.split('/').filter(Boolean).length
watch(
() => route.fullPath,
(to, from) => {
const toDepth = routeDepth(to)
const fromDepth = routeDepth(from || '/')
transitionName.value = toDepth >= fromDepth ? 'slide-left' : 'slide-right'
}
)
</script>
<template>
<NuxtLayout>
<NuxtPage :transition="{ name: transitionName, mode: 'out-in' }" />
</NuxtLayout>
</template>2.4 JavaScript 钩子增强
对于需要 JavaScript 控制的复杂动画(如 GSAP、Motion One):
<script setup lang="ts">
definePageMeta({
pageTransition: {
name: 'custom',
mode: 'out-in',
onBeforeEnter: (el) => {
el.style.opacity = '0'
el.style.transform = 'scale(0.95)'
},
onEnter: (el, done) => {
// 使用 Web Animations API 或 GSAP
el.animate(
[
{ opacity: 0, transform: 'scale(0.95)' },
{ opacity: 1, transform: 'scale(1)' },
],
{ duration: 300, easing: 'ease-out' }
).onfinish = done
},
onLeave: (el, done) => {
el.animate(
[
{ opacity: 1, transform: 'scale(1)' },
{ opacity: 0, transform: 'scale(0.95)' },
],
{ duration: 200, easing: 'ease-in' }
).onfinish = done
},
},
})
</script>3. View Transitions API 在 Nuxt4 中的集成
3.1 什么是 View Transitions API
View Transitions API 是浏览器原生的页面过渡能力,允许在 DOM 更新前后创建快照动画,实现:
- 整个页面的交叉淡入
- 共享元素的跨页面动画(如缩略图→大图)
- 无需手动管理 CSS 过渡类名
3.2 在 Nuxt4 中启用
// nuxt.config.ts
export default defineNuxtConfig({
app: {
viewTransition: true, // 启用原生 View Transitions
},
})启用后,Nuxt 在每次客户端导航时会自动调用 document.startViewTransition()。
3.3 默认效果
启用后,页面切换会自动获得交叉淡入效果(无需任何 CSS)。你可以通过 CSS 自定义这个效果:
/* 自定义 View Transition 默认动画 */
::view-transition-old(root) {
animation: 0.3s ease-out fade-out;
}
::view-transition-new(root) {
animation: 0.3s ease-in fade-in;
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}3.4 共享元素过渡
View Transitions API 的核心能力——让两个页面中的同一元素"飞"到新位置:
<!-- app/pages/videos/index.vue — 视频列表页 -->
<template>
<div class="grid grid-cols-3 gap-4">
<NuxtLink
v-for="video in videos"
:key="video.id"
:to="`/videos/${video.id}`"
>
<img
:src="video.thumbnail"
:alt="video.title"
:style="{ viewTransitionName: `video-${video.id}` }"
class="rounded-lg"
/>
<h3>{{ video.title }}</h3>
</NuxtLink>
</div>
</template><!-- app/pages/videos/[id].vue — 视频详情页 -->
<template>
<div>
<img
:src="video?.thumbnail"
:alt="video?.title"
:style="{ viewTransitionName: `video-${route.params.id}` }"
class="w-full rounded-lg"
/>
<h1 class="mt-4 text-2xl font-bold">{{ video?.title }}</h1>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const { data: video } = await useFetch(`/api/videos/${route.params.id}`)
</script>关键:两个页面中的同一元素设置相同的 view-transition-name。浏览器会自动生成从列表缩略图"飞"到详情大图的动画。
3.5 自定义共享元素动画
/* 让视频缩略图的过渡更炫 */
::view-transition-old(video-*) {
animation: 0.3s ease-out scale-down;
}
::view-transition-new(video-*) {
animation: 0.3s ease-in scale-up;
}
@keyframes scale-down {
from { transform: scale(1); }
to { transform: scale(0.8); opacity: 0; }
}
@keyframes scale-up {
from { transform: scale(0.8); opacity: 0; }
to { transform: scale(1); }
}3.6 兼容性处理
View Transitions API 目前支持 Chrome 111+、Edge 111+,Safari 和 Firefox 暂不支持。Nuxt4 会自动检测:
// Nuxt 内部逻辑(简化)
if (document.startViewTransition) {
document.startViewTransition(() => { /* 执行导航 */ })
} else {
// 降级到普通导航(无动画或使用 CSS Transition 回退)
}你不需要手动处理兼容性——不支持的浏览器会直接跳过。
4. AI 视频详情页共享元素过渡实战
4.1 效果目标
视频列表页 视频详情页
┌─────────────────┐ ┌─────────────────────────┐
│ ┌───┐ ┌───┐ │ │ ┌─────────────────────┐ │
│ │ A │ │ B │ │ 点击 B → │ │ │ │
│ └───┘ └───┘ │ 缩略图 │ │ 视频 B 大图 │ │
│ ┌───┐ ┌───┐ │ 飞向大图 │ │ │ │
│ │ C │ │ D │ │ │ └─────────────────────┘ │
│ └───┘ └───┘ │ │ 视频 B 标题... │
└─────────────────┘ └─────────────────────────┘
4.2 完整实现
<!-- app/components/VideoCard.vue -->
<script setup lang="ts">
interface Props {
video: {
id: string
title: string
thumbnail: string
duration: number
}
}
const props = defineProps<Props>()
</script>
<template>
<NuxtLink
:to="`/videos/${video.id}`"
class="group block overflow-hidden rounded-xl"
>
<div class="relative aspect-video overflow-hidden">
<img
:src="video.thumbnail"
:alt="video.title"
:style="{ viewTransitionName: `video-thumb-${video.id}` }"
class="w-full h-full object-cover transition-transform group-hover:scale-105"
/>
<span class="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-1.5 py-0.5 rounded">
{{ formatDuration(video.duration) }}
</span>
</div>
<h3
:style="{ viewTransitionName: `video-title-${video.id}` }"
class="mt-2 text-sm font-medium line-clamp-2"
>
{{ video.title }}
</h3>
</NuxtLink>
</template><!-- app/pages/videos/[id].vue -->
<script setup lang="ts">
const route = useRoute()
const { data: video } = await useFetch(`/api/videos/${route.params.id}`)
</script>
<template>
<div v-if="video" class="max-w-4xl mx-auto">
<div class="aspect-video rounded-xl overflow-hidden bg-black">
<img
:src="video.thumbnail"
:alt="video.title"
:style="{ viewTransitionName: `video-thumb-${video.id}` }"
class="w-full h-full object-cover"
/>
</div>
<h1
:style="{ viewTransitionName: `video-title-${video.id}` }"
class="mt-4 text-2xl font-bold"
>
{{ video.title }}
</h1>
<p class="mt-2 text-gray-600">{{ video.description }}</p>
</div>
</template>4.3 动画微调
/* app/assets/css/view-transitions.css */
/* 缩略图:带缩放的过渡 */
::view-transition-group(video-thumb-*) {
animation-duration: 0.35s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
/* 标题:淡入淡出 + 位移 */
::view-transition-old(video-title-*) {
animation: 0.2s ease-out forwards fade-slide-out;
}
::view-transition-new(video-title-*) {
animation: 0.3s ease-in 0.1s forwards fade-slide-in;
}
@keyframes fade-slide-out {
to { opacity: 0; transform: translateY(-10px); }
}
@keyframes fade-slide-in {
from { opacity: 0; transform: translateY(10px); }
}
/* 减少动画偏好的用户:禁用动画 */
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}本章小结
- CSS 页面过渡:
pageTransition+.{name}-enter/leave-active/from/to类名 - 过渡模式:
out-in(最常用)、in-out、默认(同时过渡) - 差异化过渡:通过
definePageMeta为每个页面指定不同效果 - 动态过渡:根据导航方向切换动画方向
- View Transitions API:
viewTransition: true一行启用,浏览器原生支持 - 共享元素:
view-transition-name匹配两个页面中的同一元素,自动生成飞行动画 - 无障碍:
prefers-reduced-motion媒体查询尊重用户偏好
下一章是路由篇的最后一章——程序化路由与深链接设计。