Skip to content

Page Transition

A click on a link changes the whole screen at once: the page leaves rising and fading, the next one arrives from below.

$npx shadcn@latest add https://framebrick.co/r/page-transition.json

From the Semono template, see it in context.

Follow a link: the whole page leaves and the next one arrives, header and footer with it.

Installation

$ npx shadcn@latest add https://framebrick.co/r/page-transition.json

Usage

// app/layout.tsx — once, and every link of the site changes
import { PageTransition } from "@/components/ui/page-transition";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <PageTransition />
        {children}
      </body>
    </html>
  );
}

// a link your own onClick handles keeps its own behaviour
<Link href="/menu" data-no-transition onClick={openMenu}>Menu</Link>

Props

PropTypeDefaultDescription
scopestring—CSS selector: only links inside an element that matches start a page change. Every link of the site by default.
timeoutnumber2500Milliseconds to wait for the new page before the transition goes on without it, so a page that never arrives cannot freeze the screen.
  • The page that is leaving fades out while it rises 30% over 0.6 s; the new one fades in from 30% lower over 0.6 s, starting 0.5 s later, so both are on screen for 0.1 s and the change takes 1.1 s. Both run on cubic-bezier(0.73, 0, 0.33, 1). The whole screen is one picture, header and footer with it, so it cannot be clicked until the change is done — as on Framer.
  • It renders only its own CSS and listens for clicks on links. It uses the browser's View Transition API and next/navigation, so it is for the App Router: router.push runs inside the transition and the new pathname lets it go on.
  • It does nothing — the browser just navigates — for a modifier key or a click that is not the main button, a link with a target other than _self, a download, another site, the same pathname, a browser without document.startViewTransition and visitors who prefer reduced motion. Back and Forward swap the page without it.
  • It listens before React does, so a link whose own onClick should handle the click needs data-no-transition, on the link or on anything around it.
  • The rules apply only while this component runs its own transition (an attribute on <html>), so an app with view transitions of its own is not touched. Semono's apply to every transition.
  • The demo uses `scope`, which keeps the effect to its two links so the rest of this page navigates as it always does.
  • Why not React's <ViewTransition>? The stable react package does not export it (Next resolves its own canary build in the App Router) and it needs canary types, so a file you copy in could not count on it. document.startViewTransition works wherever the browser has it.

AI prompt

Paste into v0, Lovable, Bolt or Claude to rebuild this component in your own project.

Create a React component called PageTransition for the Next.js App Router (no animation library). It renders only a <style> and listens for clicks.
- A "use client" component using useRouter and usePathname from next/navigation. Props: scope (a CSS selector, optional) and timeout (ms, default 2500).
- Add a click listener on document in the capture phase. Ignore the click when event.defaultPrevented, the button is not 0, a modifier key is held, document.startViewTransition does not exist, or matchMedia("(prefers-reduced-motion: reduce)") matches. Find the link with event.target.closest("a[href]"); ignore it when its target is not _self, it has a download attribute, it or an ancestor has data-no-transition, a scope is given and the link is not inside it, its origin is not the page's, or its pathname equals the current one.
- Otherwise call event.preventDefault(), set a data-page-transition attribute on <html>, and call document.startViewTransition with a callback that returns a Promise: it stores its resolve in a ref, calls router.push(pathname + search + hash) and sets a timeout (timeout ms) that resolves it as well. An effect on usePathname calls and clears the stored resolve, so the transition starts once the new page is on screen. When transition.finished settles, either way, remove the attribute.
- The <style> it renders, every rule behind :root[data-page-transition]: ::view-transition-group(root) animation-duration 250ms; ::view-transition-old(root) and ::view-transition-new(root) with mix-blend-mode normal, backface-visibility hidden, animation-duration 600ms, animation-timing-function cubic-bezier(0.73, 0, 0.33, 1) and animation-fill-mode both; old uses a keyframes from { opacity 1, translateY(0) } to { opacity 0, translateY(-30%) }; new uses from { opacity 0, translateY(30%) } to { opacity 1, translateY(0) } with animation-delay 500ms. Under prefers-reduced-motion: reduce set animation: none !important on all three.

Be the first to know

Get new templates and components first

One short email when we release a template or a free component. No spam — unsubscribe anytime.

We use your email only for Framebrick updates. Privacy policy