WebberUI

Morph Expand Card

Click a card to morph it seamlessly into a centered detail panel via shared layout; only one card in a group is expanded at a time.

Loading preview…
npx shadcn@latest add https://webberui.com/r/morph-expand-card.json

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
<MorphExpandCard />

Installation

npx shadcn@latest add https://webberui.com/r/morph-expand-card.json

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

Usage

Each MorphExpandCard holds one MorphExpandCardSummary (the collapsed content) and one MorphExpandCardDetail (the expanded detail). When wrapped in a MorphExpandGroup, only one card in the group is expanded at a time.

import {
  MorphExpandGroup,
  MorphExpandCard,
  MorphExpandCardSummary,
  MorphExpandCardDetail,
} from "@/components/ui/morph-expand-card";

<MorphExpandGroup columns={2}>
  <MorphExpandCard value="a" label="Nebula filter">
    <MorphExpandCardSummary>
      <div className="p-4 font-semibold">Nebula filter</div>
    </MorphExpandCardSummary>
    <MorphExpandCardDetail>
      <div className="p-6">The full detail content…</div>
    </MorphExpandCardDetail>
  </MorphExpandCard>
</MorphExpandGroup>;

A single card also works on its own (without a MorphExpandGroup), in which case it manages its own expand / collapse state.

Controlled mode

Pass value and onValueChange to MorphExpandGroup to control which card is expanded from outside:

const [open, setOpen] = React.useState<string | null>(null);

<MorphExpandGroup value={open} onValueChange={setOpen}>
  {/* … */}
</MorphExpandGroup>;

Custom close button

The detail panel provides a close button in the top-right corner by default. To place it yourself, pass hideDefaultClose to MorphExpandCardDetail and drop a MorphExpandCardClose anywhere in the content:

import { MorphExpandCardClose } from "@/components/ui/morph-expand-card";

<MorphExpandCardDetail hideDefaultClose>
  <MorphExpandCardClose className="left-3" />
  {/* … */}
</MorphExpandCardDetail>;

Props

MorphExpandGroup

PropTypeDefaultDescription
valuestring | nullControlled mode: the value of the currently expanded card (null collapses everything)
defaultValuestring | nullnullInitial expanded value in uncontrolled mode
onValueChange(value: string | null) => voidFires when the expanded / collapsed state changes
columnsnumber2Number of grid columns (1–6)
childrenReactNodePut MorphExpandCard children here

MorphExpandCard

PropTypeDefaultDescription
valuestringauto-generatedUnique value for the card; the group uses it to decide which one to expand
labelstringAccessible label, applied to both the trigger card and the detail dialog
childrenReactNodeMust contain one MorphExpandCardSummary and one MorphExpandCardDetail

MorphExpandCardDetail

PropTypeDefaultDescription
hideDefaultClosebooleanfalseHide the default top-right close button (use a custom MorphExpandCardClose instead)
childrenReactNodeDetail content shown while expanded

MorphExpandCardClose

PropTypeDefaultDescription
childrenReactNodeX iconCustom button content

How it works

  • The trigger card and the detail panel share one layoutId, and Motion's shared layout morphs the card's bounding box seamlessly into the panel, shrinking it back into place on collapse.
  • The detail panel is mounted onto document.body through createPortal and centered with fixed positioning, so the morph is unaffected by any parent overflow or stacking context.
  • While expanded, the card in its original position is temporarily visibility: hidden so it does not ghost behind the morphing panel.
  • Expanding locks body scrolling and restores it on collapse (or unmount); the panel is capped at 85vh and scrolls internally when the content is too long.

Accessibility

  • The trigger card is role="button" and focusable, and supports Enter / Space to expand; the detail panel is role="dialog" with aria-modal.
  • Focus moves into the panel on expand and focus returns to the trigger on collapse; Esc closes it, as does clicking the overlay.
  • Passing label sets the aria-label on both the card and the dialog.
  • When the user has "reduce motion" enabled, the morph offset drops to zero and is replaced by a brief fade in / fade out.

On this page