IInbox
Composed

Transitions

A canvas page transition — twelve coverage styles crossed with six wave orderings.

A full-viewport transition: a field of cells grows until it seals the screen, the route is swapped while the view is covered, then the wave clears off the new page.

Click any tile to play that style full-screen over this page. Pick a wave first to change the order cells reach coverage — the two axes are independent, so all 72 combinations are reachable from here.

WaveClick a tile to play it

Thumbnails are real output — each is one frame of that style caught mid-sweep, drawn by the same renderer the transition uses. Playback runs at the 300ms sweep default.

The full bench, with sweep, grow, jitter, easing and exit direction exposed as live controls, is at /lab/transitions.

Installation

npx shadcn@latest add https://ui.whoisroktim.lol/r/transitions.json

Usage

The provider has to live in a layout, not a page. Layouts persist across navigation; a page unmounts as soon as you leave it, taking the overlay with it and losing the reveal half of the animation.

app/(marketing)/layout.tsx
import { TransitionProvider, TransitionStage } from "@/components/app/transitions";

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <TransitionProvider>
      <TransitionStage>{children}</TransitionStage>
      {/* Fixed chrome goes OUTSIDE the stage — see below. */}
    </TransitionProvider>
  );
}

Then navigate with TransitionLink instead of Link:

import { TransitionLink } from "@/components/app/transitions";

<TransitionLink href="/vendors" className="...">
  Go to vendors
</TransitionLink>;

For anything that is not a link — a form submit, a redirect after an action — take navigate off the hook:

const { navigate, busy } = usePageTransition();
await save();
navigate("/done");

Two rules that are not style choices

Keep fixed chrome outside TransitionStage. The stage applies a transform so the page recedes as the curtain closes, and a transform makes an element the containing block for position: fixed descendants. A header or toolbar inside it will drift with the page instead of staying put.

The provider belongs to the layout. Covered above, but it is the single most common way to end up with a transition that covers and never uncovers.

Styles and waves

Two independent axes. Style decides how coverage is painted; wave decides the order cells reach it. Every style reads the same coverage field, so the timing contract is identical across all of them — 72 combinations.

StyleReads as
dotscircles merging
halftonestaggered print screen
ringssonar annuli
bayerordered 1-bit dither
noisegrainy film dissolve
pixelsmosaic blocks
diamondtiles on point
hexhoneycomb
blindsvenetian slats
stripesvertical slice
glyphASCII density ramp
irisone expanding circle

Waves: diagonal, left, up, center, spiral, corners.

bayer uses the same 8×8 matrix as the dither textures in scripts/, so a page that already uses that artwork transitions in the same pattern.

Defaults, and why they are where they are

{ style: "dots", cell: 26, sweep: 300, dot: 190,
  direction: "diagonal", curve: "smooth", jitter: 0.07,
  back: "retract", ink: "brand" }

The cover is the part a user actually waits through — it runs ~510ms, with the whole round trip at ~940ms. The reverse is deliberately quicker (0.65×): reverse motion should take less time than the original, because the action has already been understood.

curve redistributes the stagger rather than spreading it evenly; a linear stagger gives a ruler-straight wavefront at constant speed, which is the clearest tell that something is scripted. jitter ragged-edges the wavefront. back: "retract" inverts the delay order on the way out so the curtain undoes itself — with continue the wave carries on and exits the far edge, which suits a straight sweep and looks wrong on a centre one.

cell is per style: a dither grid wants ~9px and merging circles ~26px.

Performance

The renderer is canvas, and it has to be. One <span> per cell with its own CSS transition is 2,800 nodes and 2,800 concurrent transitions at 1080p, and over 23,000 nodes at 1440p. Canvas collapses that to a single element and one batched fill per frame, and React never re-renders during the animation.

prefers-reduced-motion skips the curtain and navigates straight through.