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

在Next.js/React中实现带URL更新的无缝页面转场动画

解决Next.js页面转场动画(指定元素更新+URL同步)

核心思路

要解决旧内容立即消失的问题,关键是延迟卸载旧页面组件直到转场动画完成;同时利用Next.js的路由API配合framer-motion的动画生命周期,实现URL同步更新的同时保留指定元素的转场效果。


实现步骤(App Router 为例,Pages Router 可适配)

避免直接使用Next.js的Link,手动控制导航流程,先触发动画再更新URL:

'use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';

export function CustomLink({ href, children }: { href: string; children: React.ReactNode }) {
  const router = useRouter();
  const [isTransitioning, setIsTransitioning] = useState(false);

  const handleNavigate = async (e: React.MouseEvent) => {
    e.preventDefault();
    setIsTransitioning(true);
    
    // 等待旧内容退出动画完成(时长需和framer-motion配置一致)
    await new Promise(resolve => setTimeout(resolve, 500));
    
    // 更新URL并导航
    router.push(href);
    setTimeout(() => setIsTransitioning(false), 100);
  };

  return (
    <a href={href} onClick={handleNavigate} className={isTransitioning ? 'transitioning' : ''}>
      {children}
    </a>
  );
}

2. 根布局中用AnimatePresence管理转场

在app/layout.tsx中包裹页面内容,通过AnimatePresence控制新旧页面的存在状态,指定需要动画的区域:

'use client';
import { AnimatePresence, motion } from 'framer-motion';
import { usePathname } from 'next/navigation';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();

  return (
    <html lang="zh-CN">
      <body>
        {/* 固定元素(如导航栏)放在AnimatePresence外,不参与转场 */}
        <header className="fixed-header">...</header>
        
        <AnimatePresence mode="wait">
          {/* 用pathname作为key,路径变化时触发动画 */}
          <motion.div
            key={pathname}
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -20 }}
            transition={{ duration: 0.5 }}
            style={{ position: 'absolute', width: '100%', top: '60px' }}
          >
            {children}
          </motion.div>
        </AnimatePresence>
      </body>
    </html>
  );
}

3. 指定局部元素转场(非整页)

如果仅需特定元素动画,可在页面组件中拆分结构,让固定内容脱离转场流程:

'use client';
import { motion, AnimatePresence } from 'framer-motion';
import { usePathname } from 'next/navigation';

export default function ProductPage() {
  const pathname = usePathname();

  return (
    <div className="page-container">
      {/* 固定侧边栏,不参与转场 */}
      <aside className="fixed-sidebar">产品分类</aside>
      
      {/* 仅动态内容区域执行转场 */}
      <AnimatePresence mode="wait">
        <motion.div
          key={pathname}
          initial={{ scale: 0.95, opacity: 0 }}
          animate={{ scale: 1, opacity: 1 }}
          exit={{ scale: 0.95, opacity: 0 }}
          transition={{ duration: 0.4 }}
        >
          <h1>产品详情</h1>
          <p>仅此区域会有转场动画</p>
        </motion.div>
      </AnimatePresence>
    </div>
  );
}

4. 适配浏览器前进/后退

监听popstate事件,同步触发对应方向的转场动画:

'use client';
import { useEffect, useState } from 'react';
import { usePathname } from 'next/navigation';
import { AnimatePresence, motion } from 'framer-motion';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();
  const [isBackNavigation, setIsBackNavigation] = useState(false);

  useEffect(() => {
    const handlePopState = () => {
      setIsBackNavigation(true);
      setTimeout(() => setIsBackNavigation(false), 500);
    };

    window.addEventListener('popstate', handlePopState);
    return () => window.removeEventListener('popstate', handlePopState);
  }, []);

  return (
    <html lang="zh-CN">
      <body>
        <AnimatePresence mode="wait">
          <motion.div
            key={pathname}
            initial={isBackNavigation ? { opacity: 0, x: -20 } : { opacity: 0, x: 20 }}
            animate={{ opacity: 1, x: 0 }}
            exit={isBackNavigation ? { opacity: 0, x: 20 } : { opacity: 0, x: -20 }}
            transition={{ duration: 0.5 }}
            style={{ position: 'absolute', width: '100%' }}
          >
            {children}
          </motion.div>
        </AnimatePresence>
      </body>
    </html>
  );
}

关键注意点

  • AnimatePresence的mode="wait":确保旧组件退出动画完成后再卸载,避免内容瞬间消失。
  • 绝对定位布局:新旧页面组件重叠,防止转场时出现布局跳动。
  • 动画时长同步:自定义导航中的setTimeout时长需和framer-motion的transition.duration保持一致。
  • Pages Router 适配:用useRouter的push方法,根组件改为_app.tsx包裹AnimatePresence,逻辑与App Router一致。

内容的提问来源于stack exchange,提问作者Tom

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 13:10:01