Skip to content
WANCRAFT

D.197Navigation & Menus

Page Transition

A route transition wrapper using the View Transitions API with a Motion fallback, direction-aware presets and SPA focus management.

NewReactMotionCSSadvanced

Overview

Page Transition wraps whatever page is current and animates to the next one whenever its pageKey changes. In browsers that support the View Transitions API it calls document.startViewTransition, holding the old page until the browser has captured it. The animation is scoped to the wrapper with its own view-transition-name and injected keyframes, so the rest of the page stays still. Elsewhere it falls back to Motion's AnimatePresence with the same presets: fade, slide (left or right depending on the direction prop), scale and wipe. It also does what client-side routers often forget. After each navigation it moves focus to the new page's heading, so keyboard and screen-reader users start at the top of the new content, and it announces the page title in a polite live region. With reduced motion every preset becomes a short fade. It has no router of its own; use it with any router, or with plain state as the demo does.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/page-transition.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/page-transition.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/page-transition.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { PageTransition } from "@/components/ui/page-transition";
import { useState } from "react";

export function PageTransitionExample() {
  const pages = ["Home", "Work", "About"];
  const [page, setPage] = useState(0);
  const [direction, setDirection] = useState<"forward" | "back">("forward");
  const go = (next: number) => {
    setDirection(next > page ? "forward" : "back");
    setPage(next);
  };

  return (
    <div>
      <nav className="flex gap-3">
        {pages.map((p, i) => (
          <button key={p} aria-current={i === page ? "page" : undefined} onClick={() => go(i)}>
            {p}
          </button>
        ))}
      </nav>
      <PageTransition pageKey={page} direction={direction}>
        <section>
          <h1>{pages[page]}</h1>
          <p>Content for {pages[page]}.</p>
        </section>
      </PageTransition>
    </div>
  );
}

03Props

PropTypeDefaultDescription
pageKeystring | number—Required. Identifies the current page; changing it plays the transition.
childrenReactNode—Required. The current page.
preset"fade" | "slide" | "scale" | "wipe""slide"Transition style (the same in both engines).
direction"forward" | "back" | "none""forward"Slide and wipe travel the other way for back.
durationnumber420Transition length in ms.
viewTransitionsbooleantrueUse document.startViewTransition when available; false always uses Motion.
manageFocusbooleantrueFocus the new page's heading (focusTarget) after navigation.
announcebooleantrueAnnounce "{title}, page loaded" in a polite live region.
titlestring—Title to announce (defaults to the focus target's text).
focusTargetstring"h1, h2, [data-page-title]"Selector for the element that gets focus on the new page.
onTransitionComplete(engine: "view-transition" | "motion") => void—Called when a transition has finished.
classNamestring—Extra classes for the wrapper.

04Features

  • 01View Transitions API when supported (feature-detected after hydration), with the old page held until the browser has captured it
  • 02Scoped: a per-instance view-transition-name and injected keyframes, so only this region animates
  • 03Motion AnimatePresence fallback (popLayout) with the same four presets
  • 04Fade, slide, scale and wipe; slide and wipe follow the direction prop
  • 05SPA focus management: focus moves to the new heading and the title is announced
  • 06Interrupt-safe: a new navigation skips the running transition
motion · tweenmotion · layoutinput · clickinput · keyboard

05Accessibility

  • After each navigation focus moves to the new page's heading (given tabindex="-1" if needed) with preventScroll, so keyboard and screen-reader users start at the new content.
  • A polite live region announces "{title}, page loaded". The title comes from the title prop or the heading text.
  • Nothing is announced or focused on first render. Only real navigations count.
  • With prefers-reduced-motion every preset becomes a short cross-fade (in both engines).
  • The wrapper adds no roles; keep your own navigation landmark and mark the current link with aria-current="page".

06Limitations

  • While a view transition runs the page shows a snapshot, so clicks are ignored for its duration (keep durations short).
  • With the View Transitions API, the animated region is a flat snapshot: inner animations pause until the transition ends.