Capítulo 20 de 24
This example animates route transitions by wrapping the router's Outlet in Framer Motion's AnimatePresence, keying it off the next route match so outgoing and incoming route components animate in and out. Each route component wraps its content in motion.div with shared transition presets.
src/main.tsx)@tanstack/react-router, framer-motion, redaxiosmainTransitionProps/postTransitionProps objects, the root route component (which renders AnimatePresence around Outlet), and the posts/$postId routes whose components are wrapped in motion.div.export const mainTransitionProps = {
initial: { y: -20, opacity: 0, position: 'absolute' },
animate: { y: 0, opacity: 1, damping: 5 },
exit: { y: 60, opacity: 0 },
transition: { type: 'spring', stiffness: 150, damping: 10 },
} as const
const rootRoute = createRootRoute({
component: () => {
const matches = useMatches()
const match = useMatch({ strict: false })
const nextMatchIndex = matches.findIndex((d) => d.id === match.id) + 1
const nextMatch = matches[nextMatchIndex]
return (
<>
{/* nav links */}
<AnimatePresence mode="wait">
<Outlet key={nextMatch.id} />
</AnimatePresence>
<TanStackRouterDevtools position="bottom-right" />
</>
)
},
})
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => (
<motion.div className="p-2" {...mainTransitionProps}>
<h3>Welcome Home!</h3>
</motion.div>
),
})
AnimatePresence's child (Outlet) by the id of the next route match, which is what makes AnimatePresence mode="wait" recognize a route change as an exit/enter transition rather than a prop update.AnimatePresence needs a stable, changing key on its direct child to detect enter/exit; deriving nextMatch.id from useMatches()/useMatch() is the trick that makes this work with the router's own match state.motion.* element (not a plain div) for its portion of the transition, since AnimatePresence only animates its immediate motion children.posts layout wrapping $postId) can nest their own AnimatePresence/Outlet pair for independent inner transitions, as shown around the posts detail outlet.postsLayoutRoute still uses a plain loader: () => fetchPosts() with useLoaderData(), no React Query involved.loader + useLoaderData().AnimatePresence, motion.div props, and transition types beyond what this example uses.