Cards & Grids

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.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add morph-expand-card.en

New project or installation trouble? Start here →

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
Active edited usageDefault props
<MorphExpandCard/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/morph-expand-card.en.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 | null—Controlled mode: the value of the currently expanded card (null collapses everything)
defaultValuestring | nullnullInitial expanded value in uncontrolled mode
onValueChange(value: string | null) => void—Fires when the expanded / collapsed state changes
columnsnumber2Number of grid columns (1–6)
childrenReactNode—Put MorphExpandCard children here

MorphExpandCard

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

MorphExpandCardDetail

PropTypeDefaultDescription
hideDefaultClosebooleanfalseHide the default top-right close button (use a custom MorphExpandCardClose instead)
childrenReactNode—Detail 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.