Page Transition

Swap views with a crossfade, a blurred one, or sections one by one.

Good morning, Maya

Here is what happened overnight.

Deploys

12

Visitors

8.4k

Errors

0

"use client"

import { Children, ViewTransition, type ReactNode } from "react"

export type PageEffect = "crossfade" | "blur" | "stagger"

export function PageTransition({
  page,
  effect = "crossfade",
  children,
}: {
  page: string
  effect?: PageEffect
  children: ReactNode
}) {
  if (effect === "stagger") {
    return (
      <div>
        {Children.toArray(children).map((section, i) => (
          <ViewTransition
            key={`${page}-${i}`}
            enter={i ? `vt-rise vt-delay-${Math.min(i, 3)}` : "vt-rise"}
            exit="vt-rise"
            default="none"
          >
            <div>{section}</div>
          </ViewTransition>
        ))}
      </div>
    )
  }
  return (
    <ViewTransition update={effect === "blur" ? "vt-blur" : "auto"}>
      <div>{children}</div>
    </ViewTransition>
  )
}

Installation

  1. 1Copy the Component tab into components/page-transition.tsx.
  2. 2Paste the CSS tab into globals.css, after Tailwind.

How it works

Three effects. crossfade, the default: a persistent boundary with the browser's own transition. blur: the same boundary, crossfading old and new with a short blur. stagger: each section gets its own boundary keyed by page, so the old sections fade out together and the new ones rise in one by one through vt-delay-*. For route navigation, use the recipe below, keyed by the URL.

Route changes in Next.js

This component swaps views inside one page. To blur between routes instead, wrap your layout's {children} in a <ViewTransition> keyed by the URL. The key makes every route change animate, nested ones included. Next.js runs navigations as transitions, so links animate with no extra code, and vt-blur is already in morph.css.

components/route-transition.tsx

// components/route-transition.tsx
"use client"

import { ViewTransition, type ReactNode } from "react"
import { usePathname } from "next/navigation"

// Keyed by the URL, so every route change animates, even /docs/a → /docs/b.
// One name for every page, so the old and new page pair into a single crossfade.
// Wrap {children} in app/layout.tsx with it.
export function RouteTransition({ children }: { children: ReactNode }) {
  const pathname = usePathname()
  return (
    <ViewTransition key={pathname} name="page" share="vt-blur" default="none">
      {children}
    </ViewTransition>
  )
}
Uses
update, vt-blur, vt-rise, vt-delay-*