在 Next.js 中实现页面切换动画:基于 next-page-transitions 的完整实践指南
页面跳转时生硬的白屏与瞬时切换常常破坏应用的使用体验。本指南以 Next.js 官方仓库中的 with-next-page-transitions 示例 为依托,系统讲解如何借助 next-page-transitions 组件为 App Router 之外的 Pages Router 应用接入优雅的页面过渡动画与"加载中"状态。读完你将掌握该库的核心 API(PageTransition、pageTransitionReadyToEnter)、CSS class 命名约定、加载指示器的时序配置,以及如何在自定义 _app.js 中将它接入 Next.js。
示例背后的核心思路:应用级组件 + CSS 驱动动画
next-page-transitions 是一个挂在应用顶层(即 Next.js 的 _app.js)的组件,职责是接管页面切换时的挂载与卸载时机。它与 react-transition-group 的思路类似:不是替你去写动画,而是在页面容器上按状态依次追加约定好的 CSS class(如 -enter、-enter-active、-exit、-exit-active),真正的过渡效果由你自己的 CSS 定义。
它在 Next.js 场景下有两个显著收益:
- 同一时刻只挂载一个页面,避免旧页面与新页面短暂并存导致的布局抖动;
- 替应用管理动画时序,让"退出旧页 → 进入新页"的时长由统一的
timeout控制。
该特性尤其适合具有共享布局元素(例如导航栏 Navbar)的应用——共享壳保持不动,只有内容区做平滑过渡,这与 Pages Router 下所有页面共享同一个 _app 的应用形态天然契合。
如何启动这个示例
方式一:使用 create-next-app 一键初始化
官方示例清单中的项目均可通过 create-next-app 直接引导。create-next-app 提供 -e, --example [name]|[github-url] 参数指定示例名,也支持用 --example-path <path-to-example> 定位示例中的子目录(见 packages/create-next-app/README.md)。
npx create-next-app --example with-next-page-transitions with-next-page-transitions-app
yarn create next-app --example with-next-page-transitions with-next-page-transitions-app
pnpm create next-app --example with-next-page-transitions with-next-page-transitions-app
以上三条命令等价,仅包管理器不同;执行后会在当前目录生成名为 with-next-page-transitions-app 的项目。
方式二:直接阅读/复用仓库中的示例目录
也可以在仓库中直接浏览 examples/with-next-page-transitions,目录结构如下:
examples/with-next-page-transitions/
├── components/
│ └── Loader.js # 页面加载期间显示的转圈组件
├── pages/
│ ├── _app.js # 全局入口,挂载 PageTransition 并定义动画 CSS
│ ├── _document.js # 自定义 Document,注入 Bootstrap 样式
│ ├── index.js # 首页(蓝底 "Hello, world!")
│ └── about.js # About 页(模拟延迟加载,演示 loading 态)
├── package.json
└── README.md
示例的 package.json 声明了三个标准脚本与依赖,next-page-transitions 的版本被固定为 1.0.0-beta.2:
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "latest",
"next-page-transitions": "1.0.0-beta.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
初始化后依次执行 npm run dev(开发)或 npm run build && npm run start(生产)即可看到两个页面:首页点击 "About us" 跳转 About 页时,会出现约 2 秒的加载动画后再淡入新页面。
核心实现一:在 _app.js 中接入 PageTransition
Pages Router 中每次路由变化都会重新渲染根组件,因此 PageTransition 要包在 _app.js 返回树的页面组件外层。完整实现见 pages/_app.js,其配置如下:
import Head from "next/head";
import { PageTransition } from "next-page-transitions";
import Loader from "../components/Loader";
const TIMEOUT = 400;
function MyApp({ Component, pageProps }) {
return (
<>
<Head>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
<PageTransition
timeout={TIMEOUT}
classNames="page-transition"
loadingComponent={<Loader />}
loadingDelay={500}
loadingTimeout={{
enter: TIMEOUT,
exit: 0,
}}
loadingClassNames="loading-indicator"
>
<Component {...pageProps} />
</PageTransition>
</>
);
}
各 prop 的含义如下表:
| Prop | 示例取值 | 作用 |
|---|---|---|
timeout |
400 |
单个过渡阶段(enter 或 exit)的时长(ms),必须与 CSS 里 transition 的时长保持一致 |
classNames |
"page-transition" |
CSS class 前缀,库会拼出 page-transition-enter、page-transition-enter-active、page-transition-exit、page-transition-exit-active |
loadingComponent |
<Loader /> |
目标页面尚未就绪时渲染的加载 UI |
loadingDelay |
500 |
触发 loading 态的延迟(ms),避免快速切换时加载条一闪而过 |
loadingTimeout |
{ enter: 400, exit: 0 } |
loading 容器自身的过渡时长;exit: 0 表示页面就绪后立即隐藏加载条 |
loadingClassNames |
"loading-indicator" |
loading 态容器的 CSS 前缀(拼出 loading-indicator-appear 等 class) |
若页面在
loadingDelay(500ms)内就绪(快速导航),加载组件根本不会出现,导航依然平滑——这正是"快则无感、慢则有反馈"体验的关键。
配套的过渡 CSS
同文件的 <style jsx global> 定义了全部动画 class。jsx global 确保样式穿透到 PageTransition 渲染出的容器上:
.page-transition-enter {
opacity: 0;
transform: translate3d(0, 20px, 0); /* 新页从下方 20px、透明开始 */
}
.page-transition-enter-active {
opacity: 1;
transform: translate3d(0, 0, 0);
transition: opacity 400ms, transform 400ms; /* 数值与 TIMEOUT=400 对齐 */
}
.page-transition-exit {
opacity: 1;
}
.page-transition-exit-active {
opacity: 0;
transition: opacity 400ms; /* 旧页只做淡出 */
}
.loading-indicator-appear,
.loading-indicator-enter {
opacity: 0;
}
.loading-indicator-appear-active,
.loading-indicator-enter-active {
opacity: 1;
transition: opacity 400ms; /* 加载指示器淡入 */
}
时序编排逻辑:路由变化时旧页先执行 -exit/-exit-active 淡出并卸载 → 若新页尚未就绪则渲染 loading 指示器 → 新页就绪后执行 -enter/-enter-active 淡入。因此 _app.js 里定义的全局 CSS 就是整个过渡"剧本",颜色、位移、时长都可自由定制。
核心实现二:页面的"延迟就绪"协议
默认情况下 PageTransition 会立即执行新页进入动画。示例的 About 页则演示了手动延迟进入,让导航先呈现 loading 态再播放进入动画。协议由两个要素构成,见 pages/about.js:
1. 静态标记 About.pageTransitionDelayEnter = true
在组件上声明该静态字段,告知 PageTransition:"本页面需要手动调用就绪回调后才允许进入",由此禁用默认的立即进入:
About.pageTransitionDelayEnter = true;
2. prop pageTransitionReadyToEnter
设置了延迟进入后,PageTransition 会向页面注入 pageTransitionReadyToEnter 回调。页面在数据/资源真正就绪后调用它,动画才会继续:
const About = (props) => {
const [loaded, setLoaded] = useState(false);
const { pageTransitionReadyToEnter } = props;
useEffect(() => {
const timeoutId = setTimeout(() => {
pageTransitionReadyToEnter(); // 通知外层:内容已就绪,可以进入
setLoaded(true);
}, 2000); // 示例中用 2s 定时器模拟数据加载
return () => {
clearTimeout(timeoutId); // 卸载时清理定时器
};
}, [pageTransitionReadyToEnter]);
if (!loaded) return null; // 未就绪时不渲染内容,交给 loading 组件占位
return ( /* ...页面内容... */ );
};
About.propTypes = {
pageTransitionReadyToEnter: PropTypes.func,
};
About.defaultProps = {
pageTransitionReadyToEnter: () => {}, // 兜底空实现,防止意外为 undefined
};
这里的要点在于把 pageTransitionReadyToEnter() 放进 useEffect 的异步回调中——真实项目中,它会出现在 fetch 数据、Image 解码或自定义 hook 的 .then() 里,即"内容真正渲染所需依赖全部就绪"的那一刻。同时通过 PropTypes 声明其类型、用 defaultProps 提供空实现兜底,避免任何一处漏传导致运行时错误。
加载指示器组件 Loader
当目标页面声明延迟进入且尚未调用就绪回调时,_app.js 中配置的 loadingComponent 便会登场。示例的加载动画是一个纯 CSS 旋转圆环,见 components/Loader.js:
const Loader = () => (
<div className="loader">
<style jsx>{`
.loader {
border: 8px solid #f3f3f3; /* 浅灰底环 */
border-top: 8px solid #3498db; /* 蓝色顶环,制造缺口 */
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 2s linear infinite; /* 匀速无限旋转 */
margin-left: auto;
margin-right: auto;
margin-top: 40px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
它由 loadingTimeout.enter(淡入,400ms)与 loadingClassNames="loading-indicator"(淡入透明度过渡)配合,在导航发起后优雅地显现。
页面与全局样式的组织方式
两个演示页面均使用 Bootstrap 色彩类做视觉区分,便于观察过渡效果,且都通过 Next.js 内置的 Link 进行客户端导航——只有客户端路由跳转才会触发过渡动画,整页刷新不会:
- pages/index.js:蓝色主色(
bg-primary)的首页,含 "Hello, world!" 标题与指向/about的链接; - pages/about.js:绿色主色(
bg-success)的 About 页,文案引导用户观察加载动画,并含返回首页的链接。
pages/_document.js 通过 <Head> 引入了 Bootstrap 4 CDN 样式表(含 SRI 完整性校验与 crossOrigin="anonymous"),并追加了 .page { height: 100vh; } 的全局规则,让每个页面充满整屏,切换时不会因内容高度差异产生跳动感。页面自身不重复引入,样式保持单一来源。
关于运行时依赖的提醒:示例依赖的
next-page-transitions(1.0.0-beta.2)为较早期的社区库,示例本身基于 Pages Router 与 React 18 编写;若要复用到当前代码库形态(例如 App Router),需自行验证该库与路由模型、React 版本的兼容性。
小结:一套可迁移的页面过渡模式
回顾示例,接入 next-page-transitions 只需三步:
- 接入:在
_app.js用PageTransition包裹<Component />,配置timeout与classNames; - 写动画:在全局样式中定义前缀对应的
-enter/-enter-active/-exit/-exit-active四类 CSS 规则,时长与timeout一致; - 可选加 loading:为"慢页面"声明
pageTransitionDelayEnter = true,数据就绪后调用注入的pageTransitionReadyToEnter(),并配置loadingComponent、loadingDelay、loadingTimeout与loadingClassNames。
这套"CSS class 状态机 + 就绪回调"的模式与是否使用 next-page-transitions 无关,即使日后替换为其他动画方案,在 Next.js 中"切换旧页 → 展示 loading → 注入就绪回调 → 淡入新页"的编排思路也依然成立。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00