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

Next.js 16.2.6 App Router中Framer Motion退出动画无法触发求助

Next.js 16.2.6 + Framer Motion 12.40 页面过渡动画问题

我用Next.js 16.2.6和Framer Motion 12.40实现页面过渡动画,需求是点击导航栏链接时,旧页面先执行退出动画,再加载新页面执行进入动画。但目前仅有进入动画正常生效,退出动画完全无法触发。


导航栏(/components/Navbar.tsx)

import Link from 'next/link'

export default function Navbar() {
  return (
    <nav className="bg-orange-950 text-gray-50 p-4 shadow-md">
      <div className="container mx-auto flex justify-between items-center">
        <Link href="/" className="text-xl font-bold">
          Company Logo
        </Link>
        <div className="space-x-6">
          <Link href="/" className="hover:text-lime-600 transition">
            Home
          </Link>
          <Link href="/services" className="hover:text-lime-600 transition">
            Services
          </Link>
          <Link href="/pricing" className="hover:text-lime-600 transition">
            Pricing
          </Link>
          <Link href="/about" className="hover:text-lime-600 transition">
            About
          </Link>
          <Link href="/contact" className="hover:text-lime-600 transition">
            Contact
          </Link>
        </div>
      </div>
    </nav>
  )
}

动画包装组件(/components/AnimateWrapper.tsx)

'use client';

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

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

return (
    <AnimatePresence mode="wait">
        <motion.div
        key={pathname}
        initial={{ opacity: 0, scale: 0.95, x:-1000 }}
        animate={{ opacity: 1, scale: 1, x:0 }}
        exit={{ opacity: 0, scale: 0.95, y:100}}
        transition={{duration: 0.7, ease: [0.25, 0.1, 0.25, 1]}}>
            {children}
        </motion.div>
    </AnimatePresence>
);
}

布局文件(/app/layout.tsx)

import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import Navbar from '@/components/Navbar'
import Footer from '@/components/Footer'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Company Website',
  description: 'Welcome to our company website',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <div className="min-h-screen flex flex-col">
          <Navbar />
          <main className="w-full">
            {children}
          </main>
          <Footer />
        </div>
      </body>
    </html>
  )
}

首页示例(/app/page.tsx)

import AnimateWrapper from '@/components/AnimateWrapper';

export default function Home() {
  return (
    <AnimateWrapper>
      <div className="w-full justify-items-center p-10 min-h-[calc(100vh-8rem)] bg-green-950 text-blue-100">
        <h1 className="text-4xl font-bold mb-6">Welcome to our company</h1>
        <div className="text-lg">
           Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tempus lectus sit amet eros lobortis, vitae interdum tortor consequat. Morbi velit urna, suscipit ac tellus ut, facilisis tristique massa. Donec mollis, risus vitae congue porttitor, augue risus egestas felis, efficitur laoreet sapien neque eget nibh. Curabitur eros massa, suscipit id sem eget, sodales imperdiet nibh. Morbi laoreet, ligula vel blandit tempus, orci velit iaculis purus, ut efficitur est magna quis libero. Proin nec mi ac mi molestie posuere non id justo. Duis a ex vitae tortor iaculis ullamcorper a in purus. Vestibulum sed cursus lectus, tempor pretium lacus. Sed fringilla porta dolor condimentum convallis. Nam ac sagittis dui, in posuere nisi. Curabitur quis scelerisque erat. Duis dapibus, risus non pretium aliquam, sapien risus accumsan ipsum, vitae fermentum mauris libero ut lorem. Nam efficitur, purus in gravida blandit, dolor nibh rutrum ex, sit amet hendrerit arcu metus vel neque.<br /><br />
           
           Maecenas odio libero, consectetur nec nibh at, maximus finibus est. Sed commodo est orci, quis consequat ipsum pellentesque sit amet. Phasellus ut leo ac elit tincidunt fringilla vel eget magna. Phasellus eu sagittis libero. Cras in magna non ipsum elementum maximus et sit amet lacus. Phasellus maximus euismod odio sed egestas. Quisque vitae libero sit amet est gravida vehicula. Ut sit amet nisi varius, pretium arcu vel, consequat libero. Quisque tortor arcu, volutpat sed eros non, pellentesque pharetra leo. Quisque malesuada risus et lobortis vestibulum.<br /><br />
           
           Pellentesque mollis, magna ac congue tristique, libero arcu fermentum dui, eget auctor lorem diam at quam. Aliquam vitae vulputate magna. Pellentesque vitae sem a ipsum commodo hendrerit non ac purus. Sed feugiat odio in tellus laoreet pretium. Nunc et lacus turpis. Phasellus pulvinar euismod mi eget molestie. Donec aliquet vestibulum hendrerit.<br /><br />

           Sed in tortor id libero consequat blandit non at ligula. Suspendisse vel convallis est. Cras porttitor elit quis pulvinar tincidunt. Mauris eu velit a metus maximus eleifend a auctor orci. Sed at tellus sit amet justo fringilla laoreet. Integer non volutpat leo, non pretium turpis. Vivamus pulvinar lorem sit amet consequat sodales.<br /><br />

           Curabitur sodales tellus sed consequat bibendum. Nunc feugiat eget quam at consectetur. Phasellus felis nunc, sagittis vel ullamcorper eu, auctor et quam. Nam condimentum urna fermentum nisl auctor laoreet. Donec ut tristique nulla. Integer ut turpis molestie, elementum ipsum id, molestie velit. Phasellus commodo, sem fermentum ultrices volutpat, est orci scelerisque ligula, at vestibulum nunc sapien eget magna. 
        </div>
      </div>
    </AnimateWrapper>
  )
}

问题原因及修复方案

核心问题

你把AnimateWrapper放在了每个页面内部,页面切换时旧页面的AnimatePresence会被直接卸载,根本没有机会执行退出动画。AnimatePresence必须在页面切换过程中保持挂载状态,才能检测到子元素的移除并触发exit动画。

修复步骤

  1. 将AnimateWrapper移到布局文件中
    修改/app/layout.tsx,让AnimatePresence包裹所有页面内容,确保它在页面切换时始终存在:

    import type { Metadata } from 'next'
    import { Inter } from 'next/font/google'
    import Navbar from '@/components/Navbar'
    import Footer from '@/components/Footer'
    import AnimateWrapper from '@/components/AnimateWrapper'
    import './globals.css'
    
    const inter = Inter({ subsets: ['latin'] })
    
    export const metadata: Metadata = {
      title: 'Company Website',
      description: 'Welcome to our company website',
    }
    
    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode
    }) {
      return (
        <html lang="en">
          <body className={inter.className}>
            <div className="min-h-screen flex flex-col">
              <Navbar />
              <main className="w-full">
                <AnimateWrapper>{children}</AnimateWrapper>
              </main>
              <Footer />
            </div>
          </body>
        </html>
      )
    }
    
  2. 移除所有页面内部的AnimateWrapper
    以首页为例,修改后不需要再包裹动画组件:

    export default function Home() {
      return (
        <div className="w-full justify-items-center p-10 min-h-[calc(100vh-8rem)] bg-green-950 text-blue-100">
          <h1 className="text-4xl font-bold mb-6">Welcome to our company</h1>
          <div className="text-lg">
             Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tempus lectus sit amet eros lobortis, vitae interdum tortor consequat. Morbi velit urna, suscipit ac tellus ut, facilisis tristique massa. Donec mollis, risus vitae congue porttitor, augue risus egestas felis, efficitur laoreet sapien neque eget nibh. Curabitur eros massa, suscipit id sem eget, sodales imperdiet nibh. Morbi laoreet, ligula vel blandit tempus, orci velit iaculis purus, ut efficitur est magna quis libero. Proin nec mi ac mi molestie posuere non id justo. Duis a ex vitae tortor iaculis ullamcorper a in purus. Vestibulum sed cursus lectus, tempor pretium lacus. Sed fringilla porta dolor condimentum convallis. Nam ac sagittis dui, in posuere nisi. Curabitur quis scelerisque erat. Duis dapibus, risus non pretium aliquam, sapien risus accumsan ipsum, vitae fermentum mauris libero ut lorem. Nam efficitur, purus in gravida blandit, dolor nibh rutrum ex, sit amet hendrerit arcu metus vel neque.<br /><br />
             
             Maecenas odio libero, consectetur nec nibh at, maximus finibus est. Sed commodo est orci, quis consequat ipsum pellentesque sit amet. Phasellus ut leo ac elit tincidunt fringilla vel eget magna. Phasellus eu sagittis libero. Cras in magna non ipsum elementum maximus et sit amet lacus. Phasellus maximus euismod odio sed egestas. Quisque vitae libero sit amet est gravida vehicula. Ut sit amet nisi varius, pretium arcu vel, consequat libero. Quisque tortor arcu, volutpat sed eros non, pellentesque pharetra leo. Quisque malesuada risus et lobortis vestibulum.<br /><br />
             
             Pellentesque mollis, magna ac congue tristique, libero arcu fermentum dui, eget auctor lorem diam at quam. Aliquam vitae vulputate magna. Pellentesque vitae sem a ipsum commodo hendrerit non ac purus. Sed feugiat odio in tellus laoreet pretium. Nunc et lacus turpis. Phasellus pulvinar euismod mi eget molestie. Donec aliquet vestibulum hendrerit.<br /><br />
    
             Sed in tortor id libero consequat blandit non at ligula. Suspendisse vel convallis est. Cras porttitor elit quis pulvinar tincidunt. Mauris eu velit a metus maximus eleifend a auctor orci. Sed at tellus sit amet justo fringilla laoreet. Integer non volutpat leo, non pretium turpis. Vivamus pulvinar lorem sit amet consequat sodales.<br /><br />
    
             Curabitur sodales tellus sed consequat bibendum. Nunc feugiat eget quam at consectetur. Phasellus felis nunc, sagittis vel ullamcorper eu, auctor et quam. Nam condimentum urna fermentum nisl auctor laoreet. Donec ut tristique nulla. Integer ut turpis molestie, elementum ipsum id, molestie velit. Phasellus commodo, sem fermentum ultrices volutpat, est orci scelerisque ligula, at vestibulum nunc sapien eget magna. 
          </div>
        </div>
      )
    }
    
  3. 验证配置有效性
    你的AnimateWrapper本身配置是正确的:mode="wait"会确保退出动画完成后再执行新页面的进入动画,key={pathname}也能正确触发组件的卸载和挂载逻辑。

额外注意事项

  • 确保所有页面都移除了内部的AnimateWrapper,避免嵌套导致的动画冲突。
  • 测试时关闭Next.js生产模式缓存,确保能看到完整的动画流程。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.02 05:07:28