WebberUI

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.

How to install Pro components →See the plans →

Loading preview…
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.

2
64
0.5
<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

PropTypeDefaultDescription
itemsViewContinuityItem[]Grid item data
columnsnumber2Number of grid columns (1–4)
openIdstring | nullControlled mode: the id of the currently expanded item (null returns to the grid)
defaultOpenIdstring | nullnullInitial expanded id in uncontrolled mode
onOpenChange(id: string | null) => voidFires on expand and on going back
spreadnumber64Base distance the other cards scatter outward (px)
durationnumber0.5Duration of a single transition (seconds)
imageAspectstring"4 / 3"Image aspect ratio (a CSS aspect-ratio value)
renderDetail(item: ViewContinuityItem) => ReactNodeCustom detail page content; renders item.description when omitted
backLabelstring"返回"Back button text and accessible label

ViewContinuityItem

FieldTypeDescription
idstringUnique identifier, the basis for the View Transition name and the controlled value
titlestringTitle shared by the card and the detail page; it carries across the transition
imagestringImage source (it morphs across the transition)
imageAltstringImage alternative text
eyebrowstringSmall line above the title (does not carry across)
descriptionReactNodeDetail page body

How it works

  • The state update is wrapped in the native document.startViewTransition() and paired with flushSync so 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 a role="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 its aria-label.
  • The transition uses only view-transition-name and CSS animation, and the animation is skipped entirely under reduce motion.

On this page