WebberUI

Data View Morph

One dataset across list, table, board, and card layouts — every entry morphs continuously to its new position with a shared-element animation, preserving the user's spatial memory.

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/data-view-morph.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.

<DataViewMorph />

Installation

npx shadcn@latest add "https://webberui.com/r/data-view-morph.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/data-view-morph.

Usage

import { DataViewMorph, type DataViewItem } from "@/components/ui/data-view-morph";

const items: DataViewItem[] = [
  { id: "auth", title: "Sign-in flow revamp", subtitle: "OAuth", group: "doing", value: 8 },
  { id: "billing", title: "Billing page rewrite", subtitle: "Annual plans", group: "todo", value: 5 },
  { id: "export", title: "Data export", subtitle: "CSV / PDF", group: "done", value: 2 },
];

<DataViewMorph
  items={items}
  groups={[
    { id: "todo", label: "To do" },
    { id: "doing", label: "In progress" },
    { id: "done", label: "Done" },
  ]}
/>;

All four layouts share the same items — on a switch, each entry uses its stable id as its layoutId to pair its before and after positions, so it flies and morphs continuously instead of fading out and being redrawn. The id must be stable (do not use the array index), otherwise the shared-element animation cannot pair entries correctly.

Controlled mode

When layout is omitted, the component ships its own toolbar and manages the layout state internally. Passing layout puts it in controlled mode, so you can supply your own triggers or sync it with URL state; pair it with showToolbar={false} to hide the built-in toolbar.

const [view, setView] = React.useState<DataViewLayout>("board");

<DataViewMorph
  items={items}
  layout={view}
  onLayoutChange={setView}
  showToolbar={false}
/>;

Props

DataViewMorph

PropTypeDefaultDescription
itemsDataViewItem[]The dataset; each entry morphs continuously across the four layouts with a shared-element animation
layoutDataViewLayoutControlled layout; when provided, the outside drives it and the internal state is disabled
defaultLayoutDataViewLayout"list"Initial layout in uncontrolled mode
onLayoutChange(layout: DataViewLayout) => voidFires on a layout switch (called in both controlled and uncontrolled modes)
layoutsDataViewLayout[]all fourWhich layouts the toolbar shows, and in what order
groupsDataViewGroup[]derived from itemsBoard column order and headings
showToolbarbooleantrueWhether to render the built-in layout-switching toolbar
headingsPartial<Record<"title" | "group" | "value", string>>Overrides for the three table column headers
classNamestringAppended to the outermost container's className

DataViewItem

FieldTypeDescription
idstringStable unique identifier; the shared-element animation pairs the same entry by it
titlestringPrimary title
subtitlestringSecondary line (optional)
groupstringGrouping key: the board splits columns by it, the other layouts show it as a group label
valuestring | numberQuantitative field (table value column, card/board corner badge; optional)
mediaReact.ReactNodeLeading media; falls back to the first character of the title when omitted (optional)

DataViewLayout

"list" | "table" | "board" | "grid" — list, table, board, and card grid respectively.

How it works

  • Shared-element morph: each entry uses its stable id as a Motion layoutId. LayoutGroup coordinates the switch, and the entry flies and scales from its old bounding box to its new position on a spring, preserving the user's spatial memory
  • No stretching: the outer frame animates its size while the inner content uses layout="position" so it is only repositioned, never scaled — text and media do not distort during large morphs, and the media frame keeps the same size in all four layouts
  • Board grouping: columns are split by group, with column order and headings taken from groups or derived from the data; each column header shows its item count, and group accent colors (dot / label) cycle by column order
  • Instance isolation: layoutIds are namespaced by the component's useId() (the LayoutGroup id), so multiple DataViewMorph instances on one page never attract each other's entries
  • Table alignment: the three columns use a fixed-width grid — 1fr is what makes cells line up across rows; the group and value column widths are fixed

Accessibility

  • The built-in toolbar is a radiogroup and each layout button is a radio, with arrow keys (←→↑↓) and Home/End switching layouts and moving focus (roving tabindex)
  • A layout switch announces the current view name through aria-live="polite"
  • The data region is a semantic list (role="list" / listitem); each board column is a role="group" with an aria-label, and the group color dots are decorative and marked aria-hidden
  • When the user has "reduce motion" enabled at the system level, the shared-element animation completes instantly (entries jump straight to their new positions) and the DOM structure stays identical to the normal state

On this page