FIXED - Move page transition from template to persistent layout so it animates
CI / quality (push) Canceled after 0s

A template.tsx is re-created on every navigation, resetting AnimatePresence
so neither enter nor exit ran. Move the transition into a PageTransition
client component rendered in the persistent (site) layout, so AnimatePresence
survives navigations and the old page animates out before the new one in.
This commit is contained in:
moh
2026-09-20 19:55:13 +02:00
parent 4df0d9ae86
commit a628b69133
2 changed files with 16 additions and 14 deletions
+4 -1
View File
@@ -4,6 +4,7 @@ import { getLocale } from "next-intl/server";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { SiteAmbientBackdrop } from "@/components/layout/site-ambient-backdrop"; import { SiteAmbientBackdrop } from "@/components/layout/site-ambient-backdrop";
import { PageTransition } from "@/components/layout/page-transition";
import { SiteDock } from "@/components/layout/site-dock"; import { SiteDock } from "@/components/layout/site-dock";
import { SiteFooter } from "@/components/layout/site-footer"; import { SiteFooter } from "@/components/layout/site-footer";
import { ScrollSmootherProvider } from "@/components/scroll-smoother-provider"; import { ScrollSmootherProvider } from "@/components/scroll-smoother-provider";
@@ -47,7 +48,9 @@ export default async function SiteLayout({ children, params }: SiteLayoutProps)
<div id="smooth-wrapper"> <div id="smooth-wrapper">
<div id="smooth-content"> <div id="smooth-content">
<div className="site-content-frame flex min-h-screen flex-col"> <div className="site-content-frame flex min-h-screen flex-col">
<main className="flex-1">{children}</main> <main className="flex-1">
<PageTransition>{children}</PageTransition>
</main>
<SiteFooter defaultLocale={siteSettings.defaultLocale} /> <SiteFooter defaultLocale={siteSettings.defaultLocale} />
</div> </div>
</div> </div>
@@ -6,15 +6,12 @@ import { usePathname } from "next/navigation";
import { useContext, useState, type ReactNode } from "react"; import { useContext, useState, type ReactNode } from "react";
/** /**
* App Router swaps in the new page's content as soon as you navigate, so the * Pins the layout-router context captured when this instance mounted. Each
* outgoing page would normally vanish before it can animate out. `FrozenRouter` * keyed page gets its own FrozenRouter, so the *outgoing* page keeps rendering
* pins the layout-router context to the value it had when this instance * its old route data while it animates out.
* mounted, keeping the *old* subtree rendered while its exit animation plays.
*/ */
function FrozenRouter({ children }: { children: ReactNode }) { function FrozenRouter({ children }: { children: ReactNode }) {
const context = useContext(LayoutRouterContext); const context = useContext(LayoutRouterContext);
// Freeze the context captured at mount so the outgoing subtree keeps
// rendering (with its old route data) while its exit animation plays.
const [frozen] = useState(context); const [frozen] = useState(context);
if (!frozen) { if (!frozen) {
@@ -25,21 +22,23 @@ function FrozenRouter({ children }: { children: ReactNode }) {
} }
/** /**
* One unified page transition on every navigation: the old page animates out * One unified page transition on every navigation. Lives in the (persistent)
* (blur + rise up) before the new page animates in (blur + rise from below). * site layout NOT in a `template.tsx`, which Next re-creates on each
* navigation and would reset AnimatePresence so nothing animates. The old page
* animates out (rise + blur), then the new page animates in from below.
*/ */
export default function SiteTemplate({ children }: { children: ReactNode }) { export function PageTransition({ children }: { children: ReactNode }) {
const reducedMotion = useReducedMotion(); const reducedMotion = useReducedMotion();
const pathname = usePathname(); const pathname = usePathname();
return ( return (
<AnimatePresence mode="wait" initial={false}> <AnimatePresence mode="wait">
<motion.div <motion.div
key={pathname} key={pathname}
initial={reducedMotion ? false : { opacity: 0, y: 18, filter: "blur(10px)" }} initial={reducedMotion ? false : { opacity: 0, y: 20, filter: "blur(10px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }} animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
exit={reducedMotion ? { opacity: 0 } : { opacity: 0, y: -18, filter: "blur(10px)" }} exit={reducedMotion ? { opacity: 0 } : { opacity: 0, y: -20, filter: "blur(10px)" }}
transition={reducedMotion ? { duration: 0 } : { duration: 0.4, ease: [0.22, 1, 0.36, 1] }} transition={reducedMotion ? { duration: 0 } : { duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
> >
<FrozenRouter>{children}</FrozenRouter> <FrozenRouter>{children}</FrozenRouter>
</motion.div> </motion.div>