View Continuity Grid
A real view transition between a card grid and a detail page — the clicked card's image and title carry across with the View Transitions API, the other cards scatter outward by distance and return in sequence on the way back.
This is a WebberUI Pro component
Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.
npx shadcn@latest add "https://webberui.com/r/view-continuity-grid.json?t=<install token>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ViewContinuityGrid />
Installation
npx shadcn@latest add "https://webberui.com/r/view-continuity-grid.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/view-continuity-grid.
Usage
Just pass an items array. Clicking a card switches to that item's detail page, with the image and title morphing across the transition via the View Transitions API while the other cards scatter outward; on the way back they return in sequence.
import {
ViewContinuityGrid,
type ViewContinuityItem,
} from "@/components/ui/view-continuity-grid";
const items: ViewContinuityItem[] = [
{
id: "aurora",
title: "Aurora Plains",
eyebrow: "69° North",
image: "/images/aurora.jpg",
description: "Body copy for the detail page…",
},
// …
];
<ViewContinuityGrid items={items} columns={2} />;Controlled mode
Pass openId and onOpenChange to drive the currently expanded item from the outside (from router state, for example); null means back to the grid:
const [openId, setOpenId] = React.useState<string | null>(null);
<ViewContinuityGrid items={items} openId={openId} onOpenChange={setOpenId} />;Custom detail content
By default the detail page renders item.description. To take full control of the detail page layout, pass renderDetail:
<ViewContinuityGrid
items={items}
renderDetail={(item) => (
<article className="prose">
<p>{item.description}</p>
<a href={`/posts/${item.id}`}>Read the full story →</a>
</article>
)}
/>;Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | ViewContinuityItem[] | — | Grid item data |
columns | number | 2 | Number of grid columns (1–4) |
openId | string | null | — | Controlled mode: the id of the currently expanded item (null returns to the grid) |
defaultOpenId | string | null | null | Initial expanded id in uncontrolled mode |
onOpenChange | (id: string | null) => void | — | Fires on expand and on going back |
spread | number | 64 | Base distance the other cards scatter outward (px) |
duration | number | 0.5 | Duration of a single transition (seconds) |
imageAspect | string | "4 / 3" | Image aspect ratio (a CSS aspect-ratio value) |
renderDetail | (item: ViewContinuityItem) => ReactNode | — | Custom detail page content; renders item.description when omitted |
backLabel | string | "返回" | Back button text and accessible label |
ViewContinuityItem
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier, the basis for the View Transition name and the controlled value |
title | string | Title shared by the card and the detail page; it carries across the transition |
image | string | Image source (it morphs across the transition) |
imageAlt | string | Image alternative text |
eyebrow | string | Small line above the title (does not carry across) |
description | ReactNode | Detail page body |
How it works
- The state update is wrapped in the native
document.startViewTransition()and paired withflushSyncso the DOM is swapped within the same frame, which is what lets the browser capture the before and after snapshots correctly. - The clicked card's image and title share one set of
view-transition-names across the two views and the browser handles the morph; names are not attached at rest and are only assigned during the transition itself, so they never interfere with other View Transitions on the page. - The other cards compute their scatter direction and distance from their grid position relative to the clicked card, with more distant cards travelling further; on the way back, dynamically injected keyframes stagger their delay by distance so they return in sequence.
- In browsers without the View Transitions API, or when the user has "reduce motion" enabled, the view switches instantly and the functionality is entirely unaffected.
Accessibility
- Cards are native
<button>elements, focusable and triggered with Enter / Space; the grid is arole="list". - Entering the detail page moves focus to the back button, and returning to the grid returns focus to the original card; the detail page supports Esc to go back.
- The detail page is a
role="region"with the item title as itsaria-label. - The transition uses only
view-transition-nameand CSS animation, and the animation is skipped entirely under reduce motion.
Era Strata Layout
An accordion layout that stacks eras or phases into horizontal strata — the expanded band opens vertically while the rest compress, and the content inside floats up with parallax.
Expose Overview
One click shrinks every section of the page into a thumbnail overview with FLIP; click a thumbnail to zoom back in, giving the page a spatial navigation map.