Page Transition
Swap views with a crossfade, a blurred one, or sections one by one.
"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
- 1Copy the
Componenttab intocomponents/page-transition.tsx. - 2Paste the
CSStab intoglobals.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-*