You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 06:27:51