6.1 KiB
name, description, metadata
| name | description | metadata | ||
|---|---|---|---|---|
| nextjs-view-transitions | Next.js 中 View Transitions API 的使用指南 |
|
Next.js 中的 View Transitions
设置
<ViewTransition> 开箱即用,支持 startTransition/Suspense 更新。若要对 <Link> 导航也启用动画:
// next.config.js
const nextConfig = {
experimental: { viewTransition: true },
};
module.exports = nextConfig;
这会为每个 <Link> 导航包裹 document.startViewTransition。任何 default="auto" 的 VT 会在每次链接点击时触发——请使用 default="none" 来防止动画冲突。
不要安装 react@canary——详见 SKILL.md 的"可用性"章节。
Next.js 实现补充
在遵循 implementation.md 时,应用以下补充内容:
步骤 2 之后: 启用上述实验性标志。
步骤 4: 在 <Link> 上使用 transitionTypes——详见下文"transitionTypes Prop"章节的用法和可用性说明。
步骤 6 之后: 对于相同路由的动态段(例如 /collection/[slug]),使用 key + name + share 模式——详见下文"相同路由动态段过渡"章节。
布局级别的 ViewTransition
如果页面有自己的 VT,不要在布局中添加包裹 {children} 的布局级 VT。 嵌套 VT 在父级 VT 内部时,enter/exit 永远不会触发——页面级的 enter/exit 会静默失效。请完全移除布局级 VT。
一个裸露的 <ViewTransition> 在布局中只有在页面没有自己的 VT 时才能正常工作。
布局在导航间持久存在——enter/exit 仅在初始挂载时触发,路由变化时不触发。不要在布局中使用类型键映射。
next/link 上的 transitionTypes Prop
无需包装组件,在服务端组件中即可工作:
<Link href="/products/1" transitionTypes={['transition-to-detail']}>查看产品</Link>
这替代了手动 onNavigate + startTransition + addTransitionType + router.push() 的模式。仅在非链接交互(按钮、表单)时保留手动 startTransition。
可用性: transitionTypes 需要启用 experimental.viewTransition: true,在 Next.js 15+ canary 构建版本和 Next.js 16+ 中可用。如果不可用,请使用 startTransition + addTransitionType + router.push()(参见下面的"程序化导航"章节)。检查方法:grep -r "transitionTypes" node_modules/next/dist/——如果没有结果,则回退到程序化导航。
程序化导航
'use client';
import { useRouter } from 'next/navigation';
import { startTransition, addTransitionType } from 'react';
function handleNavigate(href: string) {
const router = useRouter();
startTransition(() => {
addTransitionType('nav-forward');
router.push(href);
});
}
使用 router.replace 实现服务端筛选
对于通过 URL 参数在服务端重新渲染的搜索/排序/筛选,使用 startTransition + router.replace。VT 会生效,因为状态更新位于 startTransition 内部:
'use client';
import { useRouter } from 'next/navigation';
import { startTransition } from 'react';
function handleSort(sort: string) {
const router = useRouter();
startTransition(() => {
router.replace(`?sort=${sort}`);
});
}
包裹在 <ViewTransition key={item.id}> 中的列表项会以动画方式重新排序。这是 patterns.md 中客户端 useDeferredValue 模式的服务端组件替代方案。
双层模式(方向动画 + Suspense)
方向滑动动画与 Suspense 展现动画可以共存,因为它们在各自不同的时机触发。请将方向 VT 放在页面组件中(而非布局):
<ViewTransition
enter={{ "nav-forward": "slide-from-right", default: "none" }}
exit={{ "nav-forward": "slide-to-left", default: "none" }}
default="none"
>
<div>
<Suspense fallback={<ViewTransition exit="slide-down"><骨架屏 /></ViewTransition>}>
<ViewTransition enter="slide-up" default="none"><内容 /></ViewTransition>
</Suspense>
</div>
</ViewTransition>
将 loading.tsx 作为 Suspense 边界
Next.js 的 loading.tsx 是一个隐式的 <Suspense> 边界。在 loading.tsx 中,将骨架屏包裹在 <ViewTransition exit="..."> 中;在页面中,将内容包裹在 <ViewTransition enter="..." default="none"> 中:
// loading.tsx
<ViewTransition exit="slide-down"><PhotoGridSkeleton /></ViewTransition>
// page.tsx
<ViewTransition enter="slide-up" default="none"><PhotoGrid photos={photos} /></ViewTransition>
与显式 <Suspense> 规则相同:使用简单字符串 prop(而非类型映射),因为 Suspense 展现动画触发时不带过渡类型。
跨路由共享元素
// 列表页
{products.map((product) => (
<Link key={product.id} href={`/products/${product.id}`} transitionTypes={['nav-forward']}>
<ViewTransition name={`product-${product.id}`}>
<Image src={product.image} alt={product.name} width={400} height={300} />
</ViewTransition>
</Link>
))}
// 详情页——相同的 name
<ViewTransition name={`product-${product.id}`}>
<Image src={product.image} alt={product.name} width={800} height={600} />
</ViewTransition>
相同路由动态段过渡
当在同一路由的不同动态段之间导航时(例如 /collection/[slug]),页面保持挂载状态——enter/exit 永远不会触发。请使用 key + name + share:
<Suspense fallback={<骨架屏 />}>
<ViewTransition key={slug} name={`collection-${slug}`} share="auto" default="none">
<Content slug={slug} />
</ViewTransition>
</Suspense>
key={slug}在变化时强制卸载/重新挂载name+share="auto"创建共享元素的交叉淡入淡出效果- 位于
<Suspense>内部的 VT(不对 Suspense 设置 key)在加载期间保持旧内容可见
服务端组件
<ViewTransition>在服务端组件和客户端组件中均可使用<Link transitionTypes>在服务端组件中可用——无需'use client'- 程序化导航的
addTransitionType和startTransition需要客户端组件