Loading page…
Two primitives carry every arrival in the system. PageTransition sails the old view out and the new one in as the route key changes; CascadeReveal lands the content that follows, one rung at a time, top to bottom. Both move opacity and transform only, so layout never shifts under them and the browser keeps its own scroll restoration.
Use one of these links to exercise the shell you are reading. Press feedback begins on the click itself: the current route fades out while the router starts immediately, a route-shaped skeleton holds the geometry, then committed content fades and cascades into those same positions. The skeleton exchange has no vertical travel and there is no artificial navigation delay.
Choose destinations with different page geometry. The same transition also runs from the sidebar, home index, breadcrumbs, and command menu.
"use client";
import { startTransition, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { CascadeReveal, PageTransition } from "@coldharbor/webui";
// The provider calls this from a capture-phase same-origin link handler.
function navigate(href: string) {
setRequest({ id: nextId(), pathname: new URL(href, location.href).pathname });
setPhase("pending");
startTransition(() => router.push(href));
}
// Mounted once around the route slot in the persistent styleguide chrome.
const pending = phase === "pending";
const key = pending ? `pending:${request.id}` : `route:${pathname}`;
<PageTransition transitionKey={key} mode="wait" effect="fade" aria-busy={pending}>
{pending ? (
<PageSkeleton {...skeletonFor(request.pathname)} />
) : (
<CascadeReveal
cascadeKey={pathname}
targetSelector="[data-sg-route-content] > main > *"
direction="none"
interval={0.045}
delay={0.055}
>
{children}
</CascadeReveal>
)}
</PageTransition>targetSelector lets the client shell stagger rendered descendants from an opaque React Server Component route. The pages stay server components; their PageHeader and Section elements become independent in-place entry rungs after they reach the DOM.
--ch-motion-duration-fast--ch-motion-duration-base--ch-motion-ease-outThe transition key is the identity of the current view. drift is the expressive default for content-to-content swaps. effect='fade' keeps both views fixed and is the correct exchange when a skeleton already reserves the destination geometry.
Inbound
Three keys, one wrapper. mode='wait' (the default) keeps exactly one view mounted, so nothing stacks and the scroll anchor never doubles.
"use client";
import { usePathname } from "next/navigation";
import { PageTransition } from "@coldharbor/webui";
export function RouteSlot({ children, hasSkeleton }: Props) {
return (
<PageTransition
transitionKey={usePathname()}
mode="wait"
effect={hasSkeleton ? "fade" : "drift"}
>
{children}
</PageTransition>
);
}--ch-motion-duration-fast--ch-motion-duration-base--ch-motion-ease-outChildren sequence themselves without hand-indexed delays. direction='none' reveals in place; up and down add travel. cascadeOut keeps the presence boundary mounted long enough to stagger old content away before PageTransition admits the next key.
Vessel · ColdHarbor
Berth · 07
Draft · 4.2 m
Cargo · 12 UTXOs
Master · watch-only
Cleared · 06:14 UTC
Each child gets its own animated wrapper.
For grids and flex rows an extra wrapper would break: place CascadeItem yourself and the container only orchestrates. Items inherit the container's offset and blur unless they override them.
import { CascadeReveal, CascadeItem } from "@coldharbor/webui";
<CascadeReveal direction="none" cascadeOut exitDirection="none">
{rows.map((row) => <Row key={row.id} {...row} />)}
</CascadeReveal>
<CascadeReveal wrap={false} className="grid grid-cols-3 gap-2">
{tiles.map((tile) => <CascadeItem key={tile.id}>{tile.label}</CascadeItem>)}
</CascadeReveal>
// RSC route content can stay opaque to this client boundary.
<CascadeReveal
targetSelector="[data-route] > main > *"
direction="none"
cascadeOut
exitOrder="reverse"
>
{serverRenderedRoute}
</CascadeReveal>--ch-motion-duration-fast--ch-motion-duration-base--ch-motion-ease-outFor a content-to-content swap, the old cascade unwinds before the wrapper changes key, then the arriving content lands in order. For a skeleton swap, use the same pairing with effect='fade' and direction='none' so every placeholder resolves in place.
Passage 01
Departed 04:00 · Harbor Mode on
3 inputs · 2 outputs
Fee 14 sat/vB
Confirmed 6/6
<PageTransition transitionKey={pathname}>
<CascadeReveal
cascadeKey={pathname}
cascadeOut
exitOrder="reverse"
delay={0.08}
>
{children}
</CascadeReveal>
</PageTransition>--ch-motion-duration-fast--ch-motion-duration-base--ch-motion-ease-outThey answer different questions. Reach for the wrong one and you get a page that flickers or a list that stalls.
One region, one key at a time. Route changes, tab panels, wizard steps, any swap where the old content must leave before the new content belongs.
direction="none" is the skeleton-safe in-place reveal.Many siblings, one arrival. Lists, ledgers, card grids, dashboard tiles — anything that should read as filling in rather than appearing at once.
// Route arrives → PageTransition.
// Its contents land → CascadeReveal.
// A single element fading in → neither; use ScrollReveal or a plain motion.div.--ch-motion-duration-fast--ch-motion-duration-base--ch-motion-ease-outBoth primitives read useReducedMotion in JS — the CSS duration tokens do not reach them, so the bypass is explicit. The element tree and the markup never change, on the server or the client; only the timing collapses to zero. Content lands on the first frame and a key change is an instant swap, so no unfinished animation can strand content at zero opacity.
The same DOM, the same classes, the same order — only the motion is dropped.
const reduce = useReducedMotion();
// Never initial={false} — the server already painted the hidden state,
// so an element that writes no styles would keep that opacity: 0.
const transition = reduce ? { duration: 0 } : ENTER_TRANSITION;--ch-motion-duration-fast--ch-motion-duration-base--ch-motion-ease-outThe cascade on the flat dark stage.
Vessel · ColdHarbor
Berth · 07
Draft · 4.2 m
Cargo · 12 UTXOs
Master · watch-only
Cleared · 06:14 UTC
--ch-motion-duration-fast--ch-motion-duration-base--ch-motion-ease-out