MorphingDialog
A dialog that grows out of the element that opened it, keeping the focus contract.
@luc/ui/motionLive preview
A centred panel that grows out of the card it was opened from; Radix still owns focus, Escape and the unmount.
Run detail
Opens a panel from this card
<MorphingDialog>
<MorphingDialogTrigger>
<Card className="w-56 p-4">Run detail</Card>
</MorphingDialogTrigger>
{/* No className: it lands on the fixed centring frame, not the panel.
Size the panel with `width` or `panelClassName`. */}
<MorphingDialogContent width="md">
<div className="flex flex-col gap-4 p-5">
<MorphingDialogTitle>Run detail</MorphingDialogTitle>
<MorphingDialogDescription>The panel grows out of the card.</MorphingDialogDescription>
<MorphingDialogClose asChild>
<IconButton label="Close" variant="ghost" size="sm" />
</MorphingDialogClose>
</div>
</MorphingDialogContent>
</MorphingDialog>Other configurations
The same primitive, arranged differently. Each snippet is printed from the props its preview above was rendered with.
A narrow panel, one fact
A narrow panel with one fact: the panel is content-sized, and the trigger keeps its own width.
Sharpe
Opens a panel from this card
<MorphingDialog>
<MorphingDialogTrigger>
<Card className="w-56 p-4">Sharpe</Card>
</MorphingDialogTrigger>
{/* The panel is sized by `width`; the trigger's own box is unchanged, so
the morph still reads as the same object growing. */}
<MorphingDialogContent width="sm">
<div className="flex flex-col gap-4 p-5">
<MorphingDialogTitle>Sharpe</MorphingDialogTitle>
<dl className="grid grid-cols-1 gap-3">
<div>
<dt>Sharpe</dt>
<dd>1.42</dd>
</div>
</dl>
</div>
</MorphingDialogContent>
</MorphingDialog>Usage
import { MorphingDialog } from "@luc/ui/motion";<MorphingDialog>
<MorphingDialogTrigger>
<Card className="w-56 p-4">Run detail</Card>
</MorphingDialogTrigger>
{/* No className: it lands on the fixed centring frame, not the panel.
Size the panel with `width` or `panelClassName`. */}
<MorphingDialogContent width="md">
<div className="flex flex-col gap-4 p-5">
<MorphingDialogTitle>Run detail</MorphingDialogTitle>
<MorphingDialogDescription>The panel grows out of the card.</MorphingDialogDescription>
<MorphingDialogClose asChild>
<IconButton label="Close" variant="ghost" size="sm" />
</MorphingDialogClose>
</div>
</MorphingDialogContent>
</MorphingDialog>API
| Export | Kind | Signature |
|---|---|---|
MorphingDialog | 4 documented props | ({ children, open, defaultOpen, onOpenChange }: MorphingDialogProps) => import("@types/react").JSX.Element |
MorphingDialogClose | 1 documented props | import("@types/react").ForwardRefExoticComponent<RadixDialog.DialogCloseProps & import("@types/react").RefAttributes<HTMLButtonElement>> |
MorphingDialogContent | 15 documented props | ({ className, panelClassName, frameStyle, panelStyle, panelSlot, width, children, ...props }: MorphingDialogContentProps) => import("@types/react").JSX.Element |
MorphingDialogDescription | 1 documented props | import("@types/react").ForwardRefExoticComponent<RadixDialog.DialogDescriptionProps & import("@types/react").RefAttributes<HTMLParagraphElement>> |
MorphingDialogTitle | 1 documented props | import("@types/react").ForwardRefExoticComponent<RadixDialog.DialogTitleProps & import("@types/react").RefAttributes<HTMLHeadingElement>> |
MorphingDialogTrigger | export | ({ className, children, ...props }: MorphingDialogTriggerProps) => import("@types/react").JSX.Element |
MorphingDialog props
Type: MorphingDialogProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
defaultOpen? | boolean | undefined | — | |
onOpenChange? | ((open: boolean) => void) | undefined | — | |
open? | boolean | undefined | — |
MorphingDialogClose props
Type: DialogCloseProps & RefAttributes<HTMLButtonElement>
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
asChild?inherited | boolean | undefined | — |
MorphingDialogContent props
Type: MorphingDialogContentProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
asChild?inherited | boolean | undefined | — | |
children | ReactNode | — | |
deferPointerDownOutside?inherited | boolean | undefined | — | When `true`, a `'pointerdown'` event outside of the layered element will wait for the interaction's click event before dispatching, allowing third-party code to stop propagation o… |
forceMount?inherited | true | undefined | — | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
frameStyle? | CSSProperties | undefined | — | Layout for the centring wrapper, when the framing depends on a measurement rather than a breakpoint (a panel that fills a shell's workspace has to set its own insets). Plain CSS p… |
onCloseAutoFocus?inherited | ((event: Event) => void) | undefined | — | Event handler called when auto-focusing on close. Can be prevented. |
onEscapeKeyDown?inherited | ((event: KeyboardEvent) => void) | undefined | — | Event handler called when the escape key is down. Can be prevented. |
onFocusOutside?inherited | ((event: FocusOutsideEvent) => void) | undefined | — | Event handler called when the focus moves outside of the `DismissableLayer`. Can be prevented. |
onInteractOutside?inherited | ((event: PointerDownOutsideEvent | FocusOutsideEvent) => void) | undefined | — | Event handler called when an interaction happens outside the `DismissableLayer`. Specifically, when a `pointerdown` event happens outside or focus moves outside of it. Can be prev… |
onOpenAutoFocus?inherited | ((event: Event) => void) | undefined | — | Event handler called when auto-focusing on open. Can be prevented. |
onPointerDownOutside?inherited | ((event: PointerDownOutsideEvent) => void) | undefined | — | Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`. Can be prevented. |
panelClassName? | string | undefined | — | Extra classes for the panel itself (not the centring wrapper). |
panelSlot? | string | undefined | — | `data-slot` for the panel box itself. The wrapper keeps `morphing-dialog`, so a caller can name the box it actually sizes (`h-…`/`w-…` land on the panel, not on the full-viewport … |
panelStyle? | CSSProperties | undefined | — | Layout for the panel box itself. A resizable panel owns its own width/height as inline style (a `dvh`-based class cannot express "whatever the reader dragged to"), and the style h… |
width? | "none" | "sm" | "md" | "lg" | "xl" | undefined | — | Panel width preset. `none` lets the panel (or its own classes) decide. |
MorphingDialogDescription props
Type: DialogDescriptionProps & RefAttributes<HTMLParagraphElement>
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
asChild?inherited | boolean | undefined | — |
MorphingDialogTitle props
Type: DialogTitleProps & RefAttributes<HTMLHeadingElement>
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
asChild?inherited | boolean | undefined | — |
Accessibility
The accessibility contract is the one the primitive underneath the component already honours — the platform element for a control, or the ARIA pattern the component implements — and the Showcase's own pages are scanned for violations with axe.
What happens on the server, and when hydration changes it.
Family
Spatial
Depth, layering and geometry — motion that reads as physical space.
Related