# 页面转场 Page Transition > 换页时不闪白,旧页面缩小变暗退到后面,新页面从下面推上来。 分类:动效 · 角色:基础(手感,哪里都能用、不抢眼) [全部组件和流程](https://www.dachuiai.com/llms.txt) · [动效选型规则](https://www.dachuiai.com/guides/motion.md) ## 给 AI:用户把这个组件发给了你 1. 先看用户的项目:框架、页面类型、现在的风格、手机还是电脑为主。 2. 对照下面的「适合 / 不适合」判断合不合适。不合适就直接说,并从[动效选型规则](https://www.dachuiai.com/guides/motion.md)或[全部组件](https://www.dachuiai.com/llms.txt)里另挑一个。 3. 合适就按「装进项目」装好,示例里的文字和图片换成用户自己的。 4. 告诉用户用了什么、放在哪个文件、以后想改快慢、颜色、大小改哪个参数。 ## 什么时候用 - 适合:单页应用里页面不多,想有 App 的感觉。 - 不适合:后台和表单(来回切换很频繁,动画拖慢操作);多页网站每次换页整页刷新,要改用 View Transitions。 - 适合的网站:作品集、官网、活动页 - 放在:转场 - 手机上:好用:点击换页,和电脑上一样。 - 性能代价:低:切换的那一下两页同时存在。 - 常搭配:[共享元素转场 Shared Element Transition](https://www.dachuiai.com/c/shared-element.md)、[缓动 Easing](https://www.dachuiai.com/c/easing.md) ## 装进项目 - 写法:React 18 以上 + TypeScript + Tailwind CSS v4,一个文件就能用,不引用别的组件。 - 依赖:`npm i lucide-react` - 文件:原样存成 `page-transition.tsx`(比如 `src/components/page-transition.tsx`)。能跑命令就下载[原文](https://www.dachuiai.com/code/page-transition.tsx),比手抄准:`curl -fsSL -o src/components/page-transition.tsx https://www.dachuiai.com/code/page-transition.tsx`(Windows 写 `curl.exe`)。下载完看一眼第一行是 `import`,不是网页。 - 导出:`PageStage` 是可以直接用的部分;最后的 `export default` 是演示示例,只作参考。 - 图片:示例用了本站的 [/art/planet.jpg](https://www.dachuiai.com/art/planet.jpg)、[/art/fluid-teal.jpg](https://www.dachuiai.com/art/fluid-teal.jpg)、[/art/sunset.jpg](https://www.dachuiai.com/art/sunset.jpg)。换成用户自己的图;只想先看效果,可以从本站同样的路径下载,放进项目的 public 目录。 - 注意:PageStage 接收当前页 page 和 render(page),把你自己的路由状态接进去。 - 保留代码里 `prefers-reduced-motion` 的处理:用户开了「减弱动态效果」时内容照样完整显示。 - Next.js(有 app 文件夹):文件第一行加 `'use client';`。 - 用 JavaScript 不用 TypeScript:Vite 和 Next.js 都能直接用 .tsx 文件;也可以删掉类型标注,改名 .jsx。 - 没有 Tailwind:先装 Tailwind CSS v4(Vite 项目 `npm i tailwindcss @tailwindcss/vite`,vite 配置的 plugins 里加 `tailwindcss()`,入口引入的那个 CSS 文件(一般是 `src/index.css`)第一行写 `@import "tailwindcss";`),或者把类名改写成项目用的样式。 - 已经装了 Tailwind v3:别为了一个组件升级。类名大多通用,少数 v4 写法要换,比如焦点框 `outline-2` 在 v3 里要写成 `outline outline-2`。 - 不是 React(Vue、Svelte、小程序、纯 HTML):照着代码的做法改写,时长、曲线、参数保持一样。Wix、有赞这类建站平台装不进代码,告诉用户去平台自带的动效设置里找。 ## 参数 | 参数 | 默认值 | 作用 | | --- | --- | --- | | `page` | | 当前页面的名字 | | `render` | | 按页面名字画出页面内容的函数 | ## 你可以说 > 全站加 Page Transition 页面转场:换页时不闪白,旧页面缩小变暗退到后面,新页面从下面推上来。 ## 完整代码 ```tsx import {useCallback, useEffect, useRef, useState, type ReactNode} from 'react'; import {ArrowUpRight} from 'lucide-react'; /* --------------------------------------------------------------- * PageTransition 页面转场 * 换页时不闪白:旧页面缩小、变暗,退到后面;新页面从下面推上来,接住视线。 * 整个网站,像一个 App。 * PageStage 同时渲染新旧两页,动画结束后卸载旧页; * 动画途中再点别的页面,等这一段走完就接着切到最后点的那一页。 * ------------------------------------------------------------- */ const EASE = 'cubic-bezier(.76,0,.24,1)'; const MS = 760; export function PageStage({page, render}: {page: string; render: (page: string) => ReactNode}) { const [shown, setShown] = useState(page); const [next, setNext] = useState(null); const oldEl = useRef(null); const newEl = useRef(null); useEffect(() => { if (!next && page !== shown) setNext(page); }, [page, shown, next]); useEffect(() => { if (!next) return; const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches; const opts = {duration: reduce ? 1 : MS, easing: EASE, fill: 'both'} as const; oldEl.current!.animate([{transform: 'none', filter: 'brightness(1)', borderRadius: '0px'}, {transform: 'translateY(-7%) scale(.9)', filter: 'brightness(.4)', borderRadius: '28px'}], opts); const a = newEl.current!.animate([{transform: 'translateY(100%)', borderRadius: '36px 36px 0 0'}, {transform: 'none', borderRadius: '0px'}], opts); a.onfinish = () => { setShown(next); setNext(null); a.cancel(); }; return () => { a.onfinish = null; }; }, [next]); return (
{render(shown)}
{next &&
{render(next)}
}
); } /* ---------------- 示例:一个三页的小网站 ---------------- */ const PAGES: Record = { home: {nav: '首页', title: '看见更远的地方', sub: '一本关于远方的摄影集', img: '/art/planet.jpg', ink: '#fff'}, works: {nav: '作品', title: '十二张风景', sub: '从海面之下到星球之外', img: '/art/fluid-teal.jpg', ink: '#fff'}, about: {nav: '关于', title: '我们只拍光', sub: '三个人,走了十一个国家', img: '/art/sunset.jpg', ink: '#fff'}, }; function Page({id}: {id: string}) { const p = PAGES[id]; return ( ); } export default function PageTransitionDemo() { const [page, setPage] = useState('home'); const render = useCallback((id: string) => , []); return (
); } ```