ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Next.js 如何用 ViewTransition 为路由切换添加共享元素过渡动画

Next.js 如何用 ViewTransition 为路由切换添加共享元素过渡动画 Next.js 如何用 ViewTransition 为路由切换添加共享元素过渡动画【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js在 Next.js App Router 项目中路由切换会一次性替换整页内容缩略图消失了详情页的大图凭空出现两者之间没有任何视觉联系。用户点了一张照片缩略图却无法直观确认自己打开的是不是同一张图。React 19.3 提供的ViewTransition组件与浏览器的 View Transitions API 集成让你给需要延续的元素起同一个名字浏览器就会自动在它们的新旧位置和尺寸之间做动画不需要引入复杂动画库来管理挂载/卸载和坐标追踪。本指南的完整示例是一个叫Frames的照片画廊覆盖四个最常见模式共享元素 morph、Suspense 加载动画、前进/后退方向性滑动、同路由内容交叉淡入淡出。文档同时提供了 Demo 站点和完整示例代码仓库可以在浏览器中直接体验效果。准备条件使用 App Router无需任何额外配置即可工作。React 19.3 起包含ViewTransition组件和addTransitionTypeAPI本文所有代码依赖这一版本。浏览器支持限制React 的集成使用了较新的 View Transitions API 特性transition types 和view-transition-class在 Chromium 125 以及较新的 Safari、Firefox 版本中可用部分动画在 Safari 中行为可能不同。没有浏览器支持时应用正常运行只是不播放过渡动画不会报错。ViewTransition的动画由 TransitionsuseTransition、Suspense和useDeferredValue触发普通的setState调用不会触发。在 Next.js 中路由导航本身就是 Transition所以导航期间ViewTransition动画会自动激活。import { ViewTransition } from react注意ViewTransition从react包导入不是从next/...导入。共享元素 morph让缩略图长成详情页大图场景照片网格中点一张图进入/photo/[id]详情页看大图。核心做法是把网格缩略图和详情页大图分别包在ViewTransition里并使用相同的name。网格侧components/photo-grid.tsximport { ViewTransition } from react import Image from next/image import Link from next/link function PhotoGrid({ photos }) { return ( div classNamegrid grid-cols-3 gap-3 {photos.map((photo) ( Link key{photo.id} href{/photo/${photo.id}} ViewTransition name{photo-${photo.id}} Image src{photo.src} alt{photo.title} / /ViewTransition /Link ))} /div ) }详情页app/photo/[id]/photo-content.tsximport { ViewTransition } from react import Image from next/image async function PhotoContent({ id }) { const photo await getPhoto(id) return ( ViewTransition name{photo-${photo.id}} div style{{ position: relative, aspectRatio: 3 / 2 }} Image src{photo.src} alt{photo.title} fill / /div /ViewTransition ) }nameprop 建立身份React 在新旧页面中找到同名元素然后自动在它们的位置和尺寸之间做动画morph 本身不需要额外 props。点击缩略图后图片会从网格单元缩放、移动到大图位置返回时反向播放。用户看到的是同一个对象在移动而不是两个对象互换。一个需要注意的边界morph 只在目标内容与导航在同一次 commit 中渲染时播放预取缓存页面属于这种情况。如果目标先挂起显示 fallback两者无法配对内容到达时会改用进入动画而不是 morph。自定义 morph 动画可选morph 不带任何 CSS 就能工作。要自定义加上sharemorph和defaultnone。share会给该过渡加上morphclass供 CSS 伪元素选择器使用ViewTransition name{photo-${photo.id}} sharemorph defaultnone Image src{photo.src} alt{photo.title} / /ViewTransition这两个 prop 要同时加到两侧photo-grid.tsx和photo-content.tsx。文档明确了defaultnone的作用和陷阱不加defaultnone时每个带name的ViewTransition会在页面上任何过渡运行时都跑自己的交叉淡入淡出加了defaultnone之后必须保留显式的share——defaultnone且没有share时这一对元素会静默地不再 morph。配套 CSSapp/globals.css用一个 blur 关键帧柔化过渡中段的像素插值瑕疵400ms 的时长慢到能被察觉、快到感觉直接::view-transition-group(.morph) { animation-duration: 400ms; } ::view-transition-image-pair(.morph) { animation-name: via-blur; } keyframes via-blur { 30% { filter: blur(3px); } }用 Suspense reveal 动画化加载骨架详情页内容异步加载期间 Suspense 显示骨架屏数据到位后替换为真实内容。没有过渡时这次替换是瞬时的骨架消失、内容弹出。做法是把 fallback 包进带exit的ViewTransition把内容包进带enter的ViewTransitionapp/photo/[id]/page.tsximport { Suspense, ViewTransition } from react export default async function PhotoPage({ params }) { const { id } await params return ( Suspense fallback{ ViewTransition exitslide-down defaultnone PhotoContentSkeleton / /ViewTransition } ViewTransition enterslide-up defaultnone PhotoContent id{id} / /ViewTransition /Suspense ) }defaultnone在这里的作用是防止这两个ViewTransition在无关过渡比如共享元素 morph时也跟着动画。CSS 采用非对称时长退场快150ms进入的淡入慢210ms且延迟到退场完成之后才开始而位移动画跑得更长400ms。设计意图是旧内容迅速让出注意力新内容缓慢到达让用户来得及辨认:root { --duration-exit: 150ms; --duration-enter: 210ms; --duration-move: 400ms; } ::view-transition-old(.slide-down) { animation: var(--duration-exit) ease-out both fade reverse, var(--duration-exit) ease-out both slide-y reverse; } ::view-transition-new(.slide-up) { animation: var(--duration-enter) ease-in var(--duration-exit) both fade, var(--duration-move) ease-in both slide-y; } keyframes fade { from { filter: blur(3px); opacity: 0; } to { filter: blur(0); opacity: 1; } } keyframes slide-y { from { transform: translateY(10px); } to { transform: translateY(0); } }其中进入淡入上的var(--duration-exit)延迟就是让新内容等旧内容退场结束后再变可见。刷新页面即可验证骨架下滑淡出稍后真实内容上滑淡入。方向性滑动区分前进与后退导航morph 和加载动画解决不了用户看不出自己是在深入应用还是返回上一页的问题——前进和后退的导航看起来完全一样。约定是向左移 前进从左到右书写习惯中的翻页向右移 返回。第一步用Link的transitionTypesprop 给导航打标。注意过渡类型不是自动的由你根据应用的导航层级决定哪些链接算前进、哪些算返回// 网格中进入详情页前进 Link href{/photo/${photo.id}} transitionTypes{[nav-forward]} {/* photo thumbnail */} /Link // 详情页返回画廊后退 Link href/ transitionTypes{[nav-back]} ← Gallery /LinktransitionTypes是 App Router 下Link的 propNext.js v16.2.0 起可用其值通过React.addTransitionType传入导航 Transition供ViewTransition按导航类型应用不同动画。useRouter的push()和replace()也支持transitionTypes参数可用于编程式导航。第二步把每个参与页面的内容包进一个ViewTransition用enter/exit对象把过渡类型映射到方向动画// app/photo/[id]/page.tsx 和 app/page.tsx 使用同样的包装 ViewTransition enter{{ nav-forward: nav-forward, nav-back: nav-back, default: none, }} exit{{ nav-forward: nav-forward, nav-back: nav-back, default: none, }} defaultnone {/* page content */} /ViewTransitionenter和exit接受以过渡类型为键的对象。导航携带nav-forward类型时旧内容向左滑出、新内容从右侧滑入。default: none保证没有类型的过渡浏览器后退/前进、router.refresh()、Suspense reveal不产生方向动画。文档强调一条硬性要求每个参与页面都要包上同样的包装且必须放在page.tsx里而不是 layout 里——layout 在导航间是持续存在的enter/exit在那里永远不会触发。配套 CSS60px 的偏移量足以传达方向又不至于让用户追踪一个高速移动的元件::view-transition-old(.nav-forward) { --slide-offset: -60px; animation: 150ms ease-in both fade reverse, 400ms ease-in-out both slide reverse; } ::view-transition-new(.nav-forward) { --slide-offset: 60px; animation: 210ms ease-out 150ms both fade, 400ms ease-in-out both slide; } ::view-transition-old(.nav-back) { --slide-offset: 60px; animation: 150ms ease-in both fade reverse, 400ms ease-in-out both slide reverse; } ::view-transition-new(.nav-back) { --slide-offset: -60px; animation: 210ms ease-out 150ms both fade, 400ms ease-in-out both slide; } keyframes slide { from { translate: var(--slide-offset); } to { translate: 0; } }验证方式点进照片时内容向左滑点标记了nav-back的← Gallery链接时内容向右滑。锚定 header过渡时保持固定方向滑动中 header 不应跟着移动否则用户会失去空间参照。给 header 分配viewTransitionName并在 CSS 中抑制它的动画header style{{ viewTransitionName: site-header }} {/* navigation links */} /header::view-transition-group(site-header) { animation: none; z-index: 100; } ::view-transition-old(site-header) { display: none; } ::view-transition-new(site-header) { animation: none; }旧快照上的display: none防止新旧 header 短暂同时可见的闪烁z-index: 100让 header 渲染在滑动内容之上。保持过渡期间页面可交互过渡播放时::view-transition覆盖层会捕获指针事件动画期间的点击会丢失。加一条规则让事件穿透到活动页面::view-transition { pointer-events: none; }文档同时指出限制命中间测试仍会跳过带名字的参与者比如锚定的 header因此过渡时长应保持简短避免给用户频繁快速点击的元素起名。尊重 prefers-reduced-motion方向滑动模拟了视口内的物理移动是运动敏感最常见的诱因morph、reveal 和交叉淡化影响面积小或以透明度为主风险较低。最简单的做法是禁掉所有动画时长media (prefers-reduced-motion: reduce) { ::view-transition-old(*), ::view-transition-new(*), ::view-transition-group(*) { animation-duration: 0s !important; animation-delay: 0s !important; } }这样内容瞬时切换回到浏览器默认行为。同路由交叉淡入淡出切换内容而不是翻页画廊还有一个摄影师分区多个 tab 展示不同摄影师的照片但路由结构相同/collection/[slug]。点 tab 不像去了新页面更像同一个容器里换了内容方向滑动在这里是错误语义。做法是给ViewTransition设置key{slug}key 变化时 React 会把新旧内容当作退出/进入配对import { Suspense, ViewTransition } from react export default async function CollectionPage({ params }) { const { slug } await params return ( Suspense fallback{CollectionGridSkeleton /} ViewTransition key{slug} namecollection-content shareauto enterauto defaultnone CollectionGrid slug{slug} / /ViewTransition /Suspense ) }shareauto和enterauto让 React 使用其默认交叉淡化动画name给容器身份key{slug}的变化让 React 把新旧内容处理为退出/进入配对激活share而不是就地更新。切换 tab 时只有照片网格交叉淡化tab 栏和周围布局保持不动。限制与行为边界汇总文档明确给出的边界条件方便排查时对照无浏览器支持时不报错、只是不动画应用正常工作目标是 Suspense fallback 先出现时morph 配对不成立改用进入动画浏览器发起的回退后退按钮、滑动手势不携带过渡类型方向滑动不播放但两侧name匹配时共享元素 morph 仍然生效defaultnone与share必须搭配使用否则 morph 静默失效方向滑动的包装要放page.tsx放在 layout 中enter/exit永不触发过渡覆盖层期间带名字的参与者如锚定 header不参与命中间测试点击会被跳过。下一步文档把四个模式与它们回答的用户问题做了对应共享元素 morph 传达同一事物深入查看Suspense reveal 传达数据已加载方向滑动传达前进/返回同路由交叉淡化传达同一位置、不同内容。需要更多细节时可查看仓库中的 LinktransitionTypesprop 说明 和 useRouter 参考push()/replace()同样支持transitionTypes以及源指南中指向的 ReactViewTransition组件文档和完整示例 CSS 文件。【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表