FIXED - Move page transition from template to persistent layout so it animates
CI / quality (push) Canceled after 0s
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:
@@ -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>
|
||||||
Reference in New Issue
Block a user