Detent web components
By Shane Logsdon

Overlays & menus

Morphing Dialog

Implemented

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
Open dialog

Focus enters the dialog, Tab stays inside, Escape and the backdrop both close it, and focus returns to the trigger button.

Primary action

5.8 KB gzip for the class module · inlines the shared spring

Recorded states

  • pressed recorded state; no setter
  • closed recorded state; no setter
  • expanded recorded state; no setter
  • active recorded 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

  • id
  • open
  • subtitle
  • title

Properties

  • open
  • show
  • hide

Slots

  • default
  • trigger

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.tsx sha256 7543cb80f0bf…

Native files

The native module and its artifacts:

  • src/components/morphing-dialog/morphing-dialog.ts
  • src/components/morphing-dialog/styles.css
  • src/components/morphing-dialog/define.ts
  • docs/components/morphing-dialog.md
  • site/examples/morphing-dialog.ts
  • tests/browser/morphing-dialog.spec.ts
  • evidence/components/morphing-dialog.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
id———Recorded in the catalog as an attribute.
openopenbooleanfalsePresence semantics: present means true, absent means false.
subtitlesubtitlestringundefinedKebab-case attribute; camelCase property.
titletitlestringRequiredKebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
openbooleanfalseProperty-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

Events
EventUpstream propPayload typeNotes
detent-close(none - upstream had no close callback)(reason: 'escape' | 'backdrop' | 'close-button' | 'form') => voidComposed, bubbles; fires only for a user action.

Slots

Slots
SlotNotes
defaultConsumer content; rendered through a slot, never re-created.
triggerConsumer 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.
Previous: Morphing Button Next: Icon Swap

Docs

Components

Project