IMPROVED - Add page exit animation via frozen-router AnimatePresence
CI / quality (push) Canceled after 0s
CI / quality (push) Canceled after 0s
Wrap the template in AnimatePresence (mode="wait") with a FrozenRouter that pins the layout-router context, so the outgoing page animates out (blur + rise) before the incoming page animates in.
This commit is contained in:
@@ -1,27 +1,48 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { motion, useReducedMotion } from "framer-motion";
|
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
||||||
|
import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import type { ReactNode } from "react";
|
import { useContext, useState, type ReactNode } from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Next.js re-mounts a `template` on every navigation, so this wrapper runs its
|
* App Router swaps in the new page's content as soon as you navigate, so the
|
||||||
* enter animation each time the visitor moves between pages (via the dock or
|
* outgoing page would normally vanish before it can animate out. `FrozenRouter`
|
||||||
* any link) — giving one unified page transition instead of per-element
|
* pins the layout-router context to the value it had when this instance
|
||||||
* entrance animations.
|
* mounted, keeping the *old* subtree rendered while its exit animation plays.
|
||||||
|
*/
|
||||||
|
function FrozenRouter({ children }: { children: ReactNode }) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (!frozen) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <LayoutRouterContext.Provider value={frozen}>{children}</LayoutRouterContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One unified page transition on every navigation: the old page animates out
|
||||||
|
* (blur + rise up) before the new page animates in (blur + rise from below).
|
||||||
*/
|
*/
|
||||||
export default function SiteTemplate({ children }: { children: ReactNode }) {
|
export default function SiteTemplate({ children }: { children: ReactNode }) {
|
||||||
const reducedMotion = useReducedMotion();
|
const reducedMotion = useReducedMotion();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
<motion.div
|
<motion.div
|
||||||
key={pathname}
|
key={pathname}
|
||||||
initial={reducedMotion ? false : { opacity: 0, y: 18, filter: "blur(10px)" }}
|
initial={reducedMotion ? false : { opacity: 0, y: 18, filter: "blur(10px)" }}
|
||||||
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
||||||
transition={reducedMotion ? { duration: 0 } : { duration: 0.55, ease: [0.22, 1, 0.36, 1] }}
|
exit={reducedMotion ? { opacity: 0 } : { opacity: 0, y: -18, filter: "blur(10px)" }}
|
||||||
|
transition={reducedMotion ? { duration: 0 } : { duration: 0.4, ease: [0.22, 1, 0.36, 1] }}
|
||||||
>
|
>
|
||||||
{children}
|
<FrozenRouter>{children}</FrozenRouter>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user