docs / core / page transition

Page Transition

a route-level fade and lift: content enters from below and exits softly upward, with the outgoing view finishing before the next mounts. framework-neutral, keyed on whatever routeKey you hand it.

intensity · guidesrsc · client

provenance

authored in usva

layer

core / motion

intensity

guides · leads the eye through a sequence, then clears

composition

wraps the route outlet in the app shell, onceany router works, key it on the pathnamenever nested. one transition per shellnot for in-page swaps like tabs or lists, it unmounts everything under it

a11y

returns children untouched under prefers-reduced-motion · the wrapper is a plain div, no roles, no focus trappingjest-axe

dependencies

motion
live · simulated routes
users2,412uptime99.9%builds18

the summary at a glance. switch tabs and the whole view lifts out while the next one fades up from below.

props

proptypedefaultnotes
routeKeystringthe current route. usePathname() in next, location.pathname anywhere else. a change fires the transition.
childrenReactNodethe route content. the whole subtree unmounts and remounts on every key change.

get it

npx shadcn add https://usva.build/r/page-transition.jsoncopy
source · components/ui/page-transition.tsxexactly what this command copiescopy
"use client";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import type * as React from "react";

export interface PageTransitionProps {
  /** Change this per route (e.g. the pathname) to trigger the transition. */
  routeKey: string;
  children: React.ReactNode;
}

/**
 * Framework-neutral route transition: fade and lift in, soft lift out. Pass the
 * current route as `routeKey` (e.g. `usePathname()` in Next, `location.pathname`
 * elsewhere). Collapses to a plain render under reduced motion.
 */
export function PageTransition({ routeKey, children }: PageTransitionProps) {
  const reduced = useReducedMotion();
  if (reduced) return <>{children}</>;

  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={routeKey}
        initial={{ opacity: 0, y: 20 }}
        animate={{
          opacity: 1,
          y: 0,
          transition: { duration: 0.38, ease: [0.22, 1, 0.36, 1] },
        }}
        exit={{
          opacity: 0,
          y: -12,
          transition: { duration: 0.22, ease: [0.4, 0, 1, 1] },
        }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  );
}