-
I plan on migrating my app to Tanstack Router, but I wanted to know what implementing page transitions with Framer Motion would look like? React Router v6 makes animating page transitions kind of a pain compared to v5, and seemingly impossible in the case of nested routes, which is one of many reasons I want to migrate. I'm currently using React Router v6 with Thanks! |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments 19 replies
-
This article talks about how to do it with NextJs. https://blog.stackademic.com/how-to-perfect-slide-in-and-slide-out-page-transitions-in-next-js-with-framer-motion-67a2f320762 This should work the same way with Tanstack. In the PageTransitionLayout component, you will need to use useRouter from tanstack. The key prop for motion.div should be router.state.location.pathname I think |
Beta Was this translation helpful? Give feedback.
-
Okay, I think I've cracked this! The trick seems to be keeping hold of the old router context and using that when the component is on the way out. Thankfully, TSR exposes all the context stuff you need to make this work: const AnimatedOutlet = forwardRef<HTMLDivElement>((_, ref) => {
const RouterContext = getRouterContext();
const routerContext = useContext(RouterContext);
const renderedContext = useRef(routerContext);
const isPresent = useIsPresent();
if (isPresent) {
renderedContext.current = cloneDeep(routerContext);
}
return (
<motion.div ref={ref} {...transitionProps}>
<RouterContext.Provider value={renderedContext.current}>
<Outlet />
</RouterContext.Provider>
</motion.div>
);
}); Then in your parent component: const Root = () => {
const matches = useMatches();
const match = useMatch({ strict: false });
const nextMatchIndex = matches.findIndex((d) => d.id === match.id) + 1;
const nextMatch = matches[nextMatchIndex];
return (
<main>
<AnimatePresence mode="popLayout">
<AnimatedOutlet key={nextMatch.id} />
</AnimatePresence>
</main>
);
}; I haven't tested this super extensively, but initial results seem promising. Notes:
Would love to know if anyone uses this, and how you get on! |
Beta Was this translation helpful? Give feedback.
-
I can't get it to work too. |
Beta Was this translation helpful? Give feedback.
-
Here is a solution using react transition group: export const AnimatedOutlet = () => {
const matches = useMatches();
const match = useMatch({ strict: false });
const matchIndex = matches.findIndex((d) => d.id === match.id);
const nextMatchIndex =
matchIndex === matches.length - 1 ? matchIndex : matchIndex + 1;
const nextMatch = matches[nextMatchIndex];
const RouterContext = getRouterContext();
const routerContext = useContext(RouterContext);
const renderedContext = useRef(routerContext);
const isPresent = useRef(true);
if (isPresent.current) {
const clone = cloneDeep(routerContext);
clone.options.context = routerContext.options.context;
renderedContext.current = clone;
}
return (
<FadeTransition
mountOnEnter={true}
unmountOnExit={true}
appear={true}
transitionKey={nextMatch.pathname}
onEnter={() => {
isPresent.current = true;
}}
onExit={() => {
isPresent.current = false;
}}
>
<RouterContextProvider router={renderedContext.current}>
<Outlet />
</RouterContextProvider>
</FadeTransition>
);
}; export const FadeTransition = ({
transitionKey,
children,
...props
}: Props) => {
const nodeRef = useRef(null);
return (
<SwitchTransition>
<CSSTransition
{...props}
key={transitionKey}
timeout={150}
nodeRef={nodeRef}
>
<TransitionCont ref={nodeRef}>{children}</TransitionCont>
</CSSTransition>
</SwitchTransition>
);
}; |
Beta Was this translation helpful? Give feedback.
Okay, I think I've cracked this! The trick seems to be keeping hold of the old router context and using that when the component is on the way out. Thankfully, TSR exposes all the context stuff you need to make this work: