Next.js 13中使用Framer Motion实现页面过渡的问题
解决页面覆盖式滑入过渡问题
核心原因
默认页面切换时旧页面会被立即卸载,没法实现覆盖效果。要搞定这个,必须让新旧页面在过渡阶段同时存在,通过控制层级和动画时序,让新页面从一侧滑入覆盖旧页面,动画完成后再卸载旧页面。
具体实现方案(以Next.js App Router为例)
方案一:用Framer Motion快速实现(推荐)
Framer Motion封装了退出动画的保留逻辑,不用自己写复杂的路由监听。
1. 把根Layout改成客户端组件
app/layout.js开头加上'use client',并引入AnimatePresence:
'use client'; import { AnimatePresence } from 'framer-motion'; import { usePathname } from 'next/navigation'; export default function RootLayout({ children }) { const pathname = usePathname(); return ( <html lang="zh-CN"> <body style={{ margin: 0, overflow: 'hidden' }}> {/* mode="wait"确保旧页面等新页面进入动画开始后再卸载 */} <AnimatePresence mode="wait"> {/* 用路由路径当key,路由变化时触发动画 */} <div key={pathname} style={{ position: 'relative', width: '100vw', height: '100vh' }}> {children} </div> </AnimatePresence> </body> </html> ); }
2. 写一个通用过渡组件
新建app/components/PageTransition.jsx:
'use client'; import { motion } from 'framer-motion'; export default function PageTransition({ children, isHome }) { // 区分首页和内容页的动画方向 const variants = { hidden: { x: isHome ? '-100%' : '100%', opacity: 0 }, visible: { x: 0, opacity: 1, transition: { duration: 0.5, ease: 'easeInOut' } } }; return ( <motion.div initial="hidden" animate="visible" exit="hidden" variants={variants} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} > {children} </motion.div> ); }
3. 在页面中套用过渡组件
- 首页
app/page.js:
import PageTransition from './components/PageTransition'; export default function Home() { return ( <PageTransition isHome={true}> {/* 这里放你的首页内容 */} <div style={{ width: '100%', height: '100%', background: '#f0f0f0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <h1>首页</h1> </div> </PageTransition> ); }
- (content)下的页面(比如
app/(content)/blog/page.js):
import PageTransition from '../../components/PageTransition'; export default function Blog() { return ( <PageTransition isHome={false}> {/* 这里放你的内容页内容 */} <div style={{ width: '100%', height: '100%', background: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <h1>博客页</h1> </div> </PageTransition> ); }
方案二:原生CSS + 路由监听(无第三方依赖)
不想用Framer Motion的话,可以自己写路由监听和CSS动画:
1. 改造根Layout为客户端组件
app/layout.js:
'use client'; import { useState, useEffect } from 'react'; import { usePathname, useRouter } from 'next/navigation'; export default function RootLayout({ children }) { const pathname = usePathname(); const router = useRouter(); const [isTransitioning, setIsTransitioning] = useState(false); const [prevPath, setPrevPath] = useState(pathname); useEffect(() => { // 路由开始切换时标记过渡状态 const handleStart = () => setIsTransitioning(true); // 路由切换完成后,等动画结束再取消过渡状态 const handleComplete = () => { setPrevPath(pathname); setTimeout(() => setIsTransitioning(false), 500); // 时长和CSS动画保持一致 }; router.events.on('routeChangeStart', handleStart); router.events.on('routeChangeComplete', handleComplete); router.events.on('routeChangeError', handleComplete); return () => { router.events.off('routeChangeStart', handleStart); router.events.off('routeChangeComplete', handleComplete); router.events.off('routeChangeError', handleComplete); }; }, [router, pathname]); // 判断是去内容页还是回首页 const goToContent = !prevPath.includes('(content)') && pathname.includes('(content)'); const goToHome = prevPath.includes('(content)') && !pathname.includes('(content)'); return ( <html lang="zh-CN"> <body style={{ margin: 0, overflow: 'hidden' }}> <div style={{ position: 'relative', width: '100vw', height: '100vh' }}> {/* 过渡中渲染带动画的新页面,覆盖旧页面 */} {isTransitioning && ( <div className={`transition-page ${goToContent ? 'slide-in-right' : 'slide-in-left'}`} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} > {children} </div> )} {/* 非过渡状态直接渲染页面 */} {!isTransitioning && children} </div> </body> </html> ); }
2. 添加CSS动画(放在app/globals.css)
.slide-in-right { animation: slideInRight 0.5s ease-in-out forwards; } .slide-in-left { animation: slideInLeft 0.5s ease-in-out forwards; } @keyframes slideInRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideInLeft { from { transform: translateX(-100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
内容的提问来源于stack exchange,提问作者lgodev
相关产品推荐
相关产品推荐

