2026/9/7 7:49:19

Next.js React ViewTransition 实战:为商品图库实现共享元素变形、方向性导航与无动画降级

Next.js React ViewTransition 实战:为商品图库实现共享元素变形、方向性导航与无动画降级 Next.js React ViewTransition 实战为商品图库实现共享元素变形、方向性导航与无动画降级【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js本文以 Next.js 官方评测用例agent-043-view-transitions为蓝本讲解如何在一个商品图库应用中完整落地 ReactViewTransition从缩略图到详情大图的位置/尺寸变形共享元素 morph、前进/后退方向性滑动、Suspense 骨架屏到真实内容的平滑切换以及如何用 CSS 满足prefers-reduced-motion。读完你可以掌握官方 View Transitions 指南 中的四类核心模式并理解 评测断言 所定义的验收标准。任务需求四条硬性要求原始需求文档 PROMPT.md 只有一句话但信息量完整Add view transitions to this product gallery app. When navigating from the grid to a product detail page, the product image should smoothly morph between the thumbnail and the large detail image using a shared element transition. Page navigations should slide directionally. Suspense loading states should transition smoothly from skeleton to content. All animations must respect the users prefers-reduced-motion preference.拆解为四个可验证的功能点共享元素变形网格页商品缩略图与详情页大图之间平滑 morph方向性页面滑动前进/后退导航有不同的滑动方向Suspense 加载状态过渡骨架屏skeleton到内容的切换要平滑无障碍所有动画必须尊重prefers-reduced-motion。起点应用的结构该评测内置了一个极简商品图库代码结构如下路径均相对evals/evals/agent-043-view-transitions/app/page.tsx首页用Suspense fallback{ProductGridSkeleton /}包裹ProductGridapp/ProductGrid.tsx渲染商品网格每个商品是一个Link href{/product/${product.slug}}内含一个classNameproduct-image的色块占位图app/ProductSkeleton.tsx骨架屏组件app/product/[slug]/page.tsx详情页含product-hero大图色块、← Back to products 返回链接以及一个内部延迟 100ms 的异步组件ProductDetails同样被Suspense包裹fallback 为DetailsSkeletonlib/products.ts4 个商品的静态数据app/globals.css仅含网格布局与 skeleton 的pulse动画尚无任何 view transition 规则。关键观察起点应用中没有任何ViewTransition、transitionTypes或prefers-reduced-motion代码且网格图与详情图都是纯 CSS 色块product.color因此同一商品的视觉连续性只能靠共享元素变形来表达——这正是该用例要考察的核心。无需配置View Transition 开箱即用评测源码 头部注释明确指出View transitions 不需要任何next.config开关——experimental.viewTransition曾是空操作inert并已在 PR #96098 中移除文档现在写明works with no configuration。这一点与官方指南 view-transitions.mdx 一致View transitions work in the App Router with no configuration. The App Router uses React canary releases, which contain all stable React 19 changes as well as newer features likeViewTransition.React 的ViewTransition组件集成了浏览器 View Transitions API你只需给应保持一致性的元素命名浏览器自动在旧、新位置之间做动画。指南同时注明浏览器兼容边界React 的集成使用了较新的 API 特性transition types 与view-transition-class可用版本为 Chromium 125 及较新的 Safari/Firefox无浏览器支持时应用照常工作只是不播放动画——这是一个优雅的降级特性。模式一共享元素变形Thumbnail → Hero Morph官方指南将其定义为最重要的过渡模式when an object persists across a cut, it communicates continuity——对象跨路由保持存在向用户传达这是同一件东西。实现方式两侧使用相同的name网格侧把 ProductGrid.tsx 中的.product-image色块用ViewTransition name{...}包裹import { ViewTransition } from react import Link from next/link import { products } from /lib/products export function ProductGrid() { return ( div classNameproduct-grid {products.map((product) ( Link key{product.slug} href{/product/${product.slug}} classNameproduct-card transitionTypes{[nav-forward]} ViewTransition name{product-${product.slug}} div classNameproduct-image style{{ backgroundColor: product.color }} / /ViewTransition h2{product.name}/h2 p${product.price}/p /Link ))} /div ) }详情页侧把 product/[slug]/page.tsx 中的.product-hero用同一个name包裹ViewTransition name{product-${product.slug}} defaultnone div classNameproduct-hero style{{ backgroundColor: product.color }} / /ViewTransitionnameprop 创造身份identityReact 找到新旧两页中同名元素自动在它们的位置与尺寸之间做动画变形本身不需要额外 prop。点击缩略图时色块从网格单元平滑缩放并平移到详情大图位置返回时反向播放——用户看到的是一个对象在移动而不是两个对象在交换。一个重要的时序细节指南特别指出morph 只在目标内容于导航同一 commit 中渲染通常是预取过的缓存页时才会配对播放。若目标页先挂起suspends到 fallback就不会形成新旧配对内容到达时改走它自己的 enter 动画。对这个评测应用来说意味着详情页首屏若先显示DetailsSkeletonhero 色块不在 Suspense 边界内它在 fallback 之外立即渲染因此配对成立但如果把 hero 也移进异步组件morph 就会退化为 enter 动画——这是调试为什么我的 morph 没生效时的首要排查点。可选的自定义sharemorphdefaultnone变形无需任何 CSS 即可工作。要自定义它例如加 blur 柔化插值过程配合两个 propViewTransition name{product-${product.slug}} sharemorph defaultnonesharemorph会给 transition 打上morph类可用伪元素精确命中::view-transition-group(.morph) { animation-duration: 400ms; } ::view-transition-image-pair(.morph) { animation-name: via-blur; } keyframes via-blur { 30% { filter: blur(3px); } }defaultnone是关键防御不加它页面上任意一次 transition 都会让每个命名元素各自跑一遍默认的交叉淡入淡出crossfade。官方指南警告给命名对加了defaultnone之后必须保留显式的share否则the pair silently stops morphing——这是静默失效没有任何报错。模式二方向性导航Directional Slides用transitionTypes给导航打标前进与后退导航若不加以区分用户无法从动画判断自己走深了还是退回来了。官方指南的做法是在Link上打类型标签该 prop 的 API 说明见 link 组件文档useRouter 的push()/replace()同样支持// 网格 → 详情前进 Link href{/product/${product.slug}} transitionTypes{[nav-forward]} // 详情 → 网格返回 Link href/ transitionTypes{[nav-back]} classNameback-link ← Back to products /Link对应到本例ProductGrid.tsx 的链接标nav-forward详情页 顶部已有的back-link标nav-back。注意类型不是自动推断的需要根据应用自身的导航层级手动决定哪些链接是 forward、哪些是 back。把类型映射为动画enter/exit对象在两个页面的内容外层包一个映射 transition type → 动画类的ViewTransitionViewTransition 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 */} /ViewTransition导航携带nav-forward类型时旧内容向左滑出、新内容从右侧滑入nav-back则相反。default: none保证未携带类型的 transition浏览器返回键、router.refresh()、Suspense reveal不触发方向性动画。指南强调了一个易错点包装器必须放在page.tsx而不是 layout 里——layout 在导航间持续存在其 enter/exit 永远不会触发。本例的 layout.tsx 是纯 RootLayout天然不适合。方向性 CSS::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; } }60px 的位移足够传达方向又不至于让用户追着快速移动的元素看。另外注意浏览器触发的返回后退键/滑动手势不携带 transition type方向性滑动不会播放——但模式一的共享元素 morph 仍然生效因为 morph 靠name配对不依赖类型。模式三Suspense 骨架屏的 Reveal 过渡起点应用的两个 Suspense 边界app/page.tsx 的ProductGridSkeleton、详情页 的DetailsSkeleton目前都是瞬时替换骨架消失、内容闪现。指南的语义是垂直方向编码层级——上滑传达到达下滑传达离开两者构成一次交接handoff。写法fallback 用exit动画内容用enter动画import { Suspense, ViewTransition } from react Suspense fallback{ ViewTransition exitslide-down defaultnone ProductGridSkeleton / /ViewTransition } ViewTransition enterslide-up defaultnone ProductGrid / /ViewTransition /Suspense详情页的Suspense fallback{DetailsSkeleton /}同理处理。defaultnone防止这对元素在无关 transition如共享元素 morph中误触动画。配套的 CSS 采用非对称时序——这是指南刻意设计的节奏: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); } }设计意图旧内容骨架屏要快速离开150ms以免与新内容争抢注意力新内容更平缓地到达且 enter 的淡入动画延迟了var(--duration-exit)毫秒——即旧内容完全退场后新内容才可见。本例 globals.css 中骨架屏现有的pulse无限循环动画不受影响它与 view transition 规则正交。模式四尊重 prefers-reduced-motionPROMPT.md 的第四条要求也是 EVAL.ts 中唯一检查 CSS 而非 TSX 的测试。评测注释点破了一个关键误区React does NOT disable animations automatically——prefers-reduced-motion完全由你的 CSS 负责处理。方向性滑动是模拟跨越视口的物理位移是最常见的 motion-sickness 触发源必须降级。指南给出的最简方案是归零所有动画时长media (prefers-reduced-motion: reduce) { ::view-transition-old(*), ::view-transition-new(*), ::view-transition-group(*) { animation-duration: 0s !important; animation-delay: 0s !important; } }无动画时内容瞬间切换这正是浏览器的默认行为。指南也提到更精细的做法保留 crossfade 与 opacity 过渡、只移除位置类位移对应其Next steps章节引用的外部讨论主题即无运动并不总等于 prefers-reduced-motion。评测如何验收从 EVAL.ts 看完整检查清单EVAL.ts 用 7 个 vitest 用例对app、lib、components、src目录下的 TS/TSX 源码剥离注释后做正则匹配和全部 CSS 文件做静态检查。它既是验收标准也是常见错误清单断言校验点对应本文章节ViewTransition is imported from react必须import { ViewTransition } from react而非手动调用document.startViewTransition或从第三方库导入前置条件Shared element transitions use named ViewTransition源码中至少 2 处ViewTransition name...网格侧 详情侧成对出现模式一Link uses transitionTypes for directional navigation源码出现transitionTypes模式二Suspense content uses ViewTransition with enter or exit存在enter或exit用法模式三defaultnone prevents unintended animations存在defaultnone防止页面每次 transition 都整页 crossfade模式一/三CSS handles prefers-reduced-motionCSS 中出现prefers-reduced-motion模式四CSS defines view transition animationsCSS 使用::view-transition-old/new/group伪元素模式一~四评测头部注释还点名了 Agent 的典型翻车方式值得逐条对照绕过 React 组件直接调document.startViewTransition而不是声明式ViewTransition错误的导入来源从第三方库而非react导入不知道next/link有transitionTypesprop方向性导航无从谈起忘记defaultnone结果是页面上每次 transition 都让所有命名元素交叉淡入淡出视觉上全页面在闪跳过 reduced-motion如前所述React 不会替你禁用动画。四种模式与用户心智模型官方指南结尾用一张表总结每种模式回答的用户问题对本例完全适用模式传达的信号本例落点Shared element (morph)Same thing, going deeper网格.product-image↔ 详情.product-heroSuspense revealData loaded两处骨架屏 → 真实内容Directional slideGoing forward / coming backnav-forward/nav-back标签Same-route crossfadeSame place, different content本例未涉及可参考指南 Step 4 的keyshareauto模式另有两个通用细节来自指南落地时值得采纳其一方向性滑动期间::view-transition覆盖层会吞掉指针事件加一条::view-transition { pointer-events: none; }可让动画期间页面保持可点击named 参与者的命中测试在动画期间仍会被跳过因此保持过渡短小其二若有跨导航的固定 header给它viewTransitionName并在 CSS 中animation: nonez-index提权避免滑动时 header 跟着走而破坏空间锚点——本例 layout.tsx 没有 header可跳过。小结零配置App Router 下 React View Transition 无需next.config开关不支持的浏览器中应用照常运行。name建立身份同名ViewTransition在新旧页面间自动 morph自定义时sharedefaultnone必须成对出现。transitionTypes声明方向手动在Link上打nav-forward/nav-back标签enter/exit对象把类型映射到 CSS 类包装器放page.tsx而非 layout。Suspense reveal 用非对称时序exit 150ms 快退场enter 延迟一个 exit 时长再淡入传达交接。无障碍必须手写media (prefers-reduced-motion: reduce)下将::view-transition-*的animation-duration/delay归零。验收标准可机器检查EVAL.ts 的 7 条正则断言可直接作为自测清单。延伸阅读仓库内的官方指南全文docs/01-app/02-guides/view-transitions.mdx。【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考