Overlays & menus
Morphing Dialog
A card that expands into a dialog
detent-morphing-dialog
Port status
- Catalog status
- implemented
- Origin
- upstream
- Phase
- 1
- Wave
- phase-1-pilot
- Group
- Overlays & menus
Focus enters the dialog, Tab stays inside, Escape and the backdrop both close it, and focus returns to the trigger button.
5.8 KB gzip for the class module · inlines the shared spring
Recorded states
pressedrecorded state; no setterclosedrecorded state; no setterexpandedrecorded state; no setteractiverecorded state; no setter
Usage
Import
import '/components/morphing-dialog/define.js';
// or register it yourself
import { DetentMorphingDialog } from '/components/morphing-dialog/morphing-dialog.js';
Attributes
idopensubtitletitle
Properties
openshowhide
Slots
defaulttrigger
Events
detent-close— {}
Example
Runnable example: site/examples/morphing-dialog.ts. The snippet below is generated from the catalogued attributes.
<detent-morphing-dialog
id
open
subtitle
title="Required"
></detent-morphing-dialog>
Source
The pinned upstream source for this row:
src/components/ui/MorphingDialog.tsxsha256 7543cb80f0bf…
Native files
The native module and its artifacts:
src/components/morphing-dialog/morphing-dialog.tssrc/components/morphing-dialog/styles.csssrc/components/morphing-dialog/define.tsdocs/components/morphing-dialog.mdsite/examples/morphing-dialog.tstests/browser/morphing-dialog.spec.tsevidence/components/morphing-dialog.md
API
Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.
Attributes
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
id | — | — | — | Recorded in the catalog as an attribute. |
open | open | boolean | false | Presence semantics: present means true, absent means false. |
subtitle | subtitle | string | undefined | Kebab-case attribute; camelCase property. |
title | title | string | Required | Kebab-case attribute; camelCase property. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
open | boolean | false | Property-only: assign from script; not accepted as an attribute. |
show | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
hide | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
Events
| Event | Upstream prop | Payload type | Notes |
|---|---|---|---|
detent-close | (none - upstream had no close callback) | (reason: 'escape' | 'backdrop' | 'close-button' | 'form') => void | Composed, bubbles; fires only for a user action. |
Slots
| Slot | Notes |
|---|---|
default | Consumer content; rendered through a slot, never re-created. |
trigger | Consumer content; rendered through a slot, never re-created. |
CSS customisation
- React `children`/`ReactNode` props become DOM slots; caller content stays light DOM.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- Upstream `trigger: (open) => ReactNode` render function is a named slot `slot="trigger"`; a render function is neither an attribute nor a serialisable property, and a slot keeps the consumer button's own semantics, focus, keyboard activation and listeners.
- Added `detent-close` event ({ detail: { reason: escape | backdrop | close-button | form } }, bubbles + composed, not cancelable) because upstream exposed no close callback.
- Added `open` attribute/property plus `show()`/`hide()`; programmatic open/hide emit no events.
- Framer Motion layoutId morph replaced by the shared springMorph spring (220/32/1) driving one FLIP progress value, so interruption retargets instead of restarting; still a spring, so PC-10.4 is not triggered.
- No per-child scale correction; content and backdrop opacity follow the morph progress instead of the upstream 0.05/0.1/0.15s delay chain and easeSoft.
- Native <dialog> + showModal() top layer and native cancel, with explicit focus entry, Tab containment and focus return (upstream claimed but did not implement trapping or aria-modal).
- Scroll lock applied to documentElement and restored to its previous value instead of body/unset.
- lucide-react X replaced by a local inline SVG.
- Surface palette reads the --detent-color-* theme tokens with the upstream dark hex values as fallbacks, so the dialog is legible in both themes; upstream pinned a dark palette because its site was dark-only. Every --detent-morphing-dialog-* property still overrides per instance.
What it does
A slotted trigger opens a modal dialog whose surface morphs out of the trigger box; Escape, backdrop activation and the close button dismiss it exactly once and return focus to the trigger.
Upstream behaviour
- Framer Motion layoutId continuous surface expansion
- Esc key dismissal and backdrop click support
- Body scroll lock handling during active state
Differences from upstream
- React `children`/`ReactNode` props become DOM slots; caller content stays light DOM.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- Upstream `trigger: (open) => ReactNode` render function is a named slot `slot="trigger"`; a render function is neither an attribute nor a serialisable property, and a slot keeps the consumer button's own semantics, focus, keyboard activation and listeners.
- Added `detent-close` event ({ detail: { reason: escape | backdrop | close-button | form } }, bubbles + composed, not cancelable) because upstream exposed no close callback.
- Added `open` attribute/property plus `show()`/`hide()`; programmatic open/hide emit no events.
- Framer Motion layoutId morph replaced by the shared springMorph spring (220/32/1) driving one FLIP progress value, so interruption retargets instead of restarting; still a spring, so PC-10.4 is not triggered.
- No per-child scale correction; content and backdrop opacity follow the morph progress instead of the upstream 0.05/0.1/0.15s delay chain and easeSoft.
- Native <dialog> + showModal() top layer and native cancel, with explicit focus entry, Tab containment and focus return (upstream claimed but did not implement trapping or aria-modal).
- Scroll lock applied to documentElement and restored to its previous value instead of body/unset.
- lucide-react X replaced by a local inline SVG.
- Surface palette reads the --detent-color-* theme tokens with the upstream dark hex values as fallbacks, so the dialog is legible in both themes; upstream pinned a dark palette because its site was dark-only. Every --detent-morphing-dialog-* property still overrides per instance.