Buttons & actions
Morphing Button
Dimension-preserving state morphing button with fluid icon transitions
detent-morphing-button
Port status
- Catalog status
- implemented
- Origin
- upstream
- Phase
- 4
- Wave
- phase-4-motion-controls
- Group
- Buttons & actions
6.3 KB gzip for the class module · inlines the shared spring
Variants
Local demo; no request leaves the page. Click the primary button above the Variants heading to watch a simulated
deployment: it moves to
loading (spinner, aria-busy="true" and the button disabled), then to
success after 1.2 s. Every state occupies exactly the same box — a hidden sizer reserves the widest
label — and the label and icon cross-fade through one spring. A rapid sequence of status changes retargets that
spring instead of stacking. Each activation emits detent-click with
{ button, shiftKey, ctrlKey, altKey, metaKey }; under prefers-reduced-motion: reduce the
states change instantly and the press scale stays at 1.
No activation yet.
Recorded states
focus-visiblerecorded state; no setterpressedrecorded state; no setterdisabledrecorded state; no setterloadingrecorded state; no settererrorrecorded state; no settercompletedrecorded state; no setteridlerecorded state; no settersuccessrecorded state; no setter
Usage
Import
import '/components/morphing-button/define.js';
// or register it yourself
import { DetentMorphingButton } from '/components/morphing-button/morphing-button.js';
Attributes
disablederror-textidle-textloading-textstatussuccess-textvariant
Properties
onClick
Slots
error-iconidle-iconsuccess-icon
Events
No events are recorded for this component.
Example
Runnable example: site/examples/morphing-button.ts. The snippet below is generated from the catalogued attributes.
<detent-morphing-button
disabled
error-text="Failed"
idle-text="Save Changes"
loading-text="Saving..."
status="idle"
success-text="Saved"
variant="primary"
></detent-morphing-button>
Source
The pinned upstream source for this row:
src/components/ui/MorphingButton.tsxsha256 db6a57f39976…
Native files
The native module and its artifacts:
src/components/morphing-button/morphing-button.tssrc/components/morphing-button/styles.csssrc/components/morphing-button/define.tsdocs/components/morphing-button.mdsite/examples/morphing-button.tstests/browser/morphing-button.spec.tsevidence/components/morphing-button.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 |
|---|---|---|---|---|
disabled | — | — | — | Recorded in the catalog as an attribute. |
error-text | errorText | string | 'Failed' | Kebab-case attribute; camelCase property. |
idle-text | idleText | string | 'Save Changes' | Kebab-case attribute; camelCase property. |
loading-text | loadingText | string | 'Saving...' | Kebab-case attribute; camelCase property. |
status | status | 'idle' | 'loading' | 'success' | 'error' | 'idle' | Kebab-case attribute; camelCase property. |
success-text | successText | string | 'Saved' | Kebab-case attribute; camelCase property. |
variant | variant | 'primary' | 'secondary' | 'danger' | 'ghost' | 'primary' | Kebab-case attribute; camelCase property. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
onClick | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
Events
No custom events are recorded for this component.
Slots
| Slot | Notes |
|---|---|
error-icon | Consumer content; rendered through a slot, never re-created. |
idle-icon | Consumer content; rendered through a slot, never re-created. |
success-icon | Consumer content; rendered through a slot, never re-created. |
CSS customisation
- 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.
- disabled is an attribute and a property: the row records the disabled state and the pinned source takes it from ButtonHTMLAttributes, so the state would otherwise be unreachable from HTML.
- success and error keep the shared text weight so the box cannot re-measure (upstream used font-semibold); the tone colour carries the state.
- A hidden sizer stacking the four labels reserves the widest width, which is what implements the recorded zero-layout-shift feature.
- primary is the page inverse (text on background) and flips with the scheme; secondary, danger and ghost follow tokens; the boundary is a text/background mix for WCAG 1.4.11.
- The morph is one spring progress (out, swap at the transparent point, in) instead of AnimatePresence mode=wait, so interruptions collapse intermediate labels.
- Activation is the native composed click and the onClick property hook; the detent-click companion the row originally recorded was dropped on 2026-10-26 after the PC-6.1 review (a native event is never re-emitted under a custom name), and the row is corrected here.
What it does
An action button whose label and icon morph between four states — idle, loading, success and error — inside a box that never changes size.
Upstream behaviour
- Zero layout shift architecture preserving natural bounding dimensions across states
- Icon morphing with spring-based scale and translation transitions
- Multiple design presets (Primary, Secondary, Danger, and Ghost)
- Interactive spring tap physics (whileTap scale 0.97)
- Complete disabled and busy ARIA state compatibility
Differences from upstream
- 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.
- disabled is an attribute and a property: the row records the disabled state and the pinned source takes it from ButtonHTMLAttributes, so the state would otherwise be unreachable from HTML.
- success and error keep the shared text weight so the box cannot re-measure (upstream used font-semibold); the tone colour carries the state.
- A hidden sizer stacking the four labels reserves the widest width, which is what implements the recorded zero-layout-shift feature.
- primary is the page inverse (text on background) and flips with the scheme; secondary, danger and ghost follow tokens; the boundary is a text/background mix for WCAG 1.4.11.
- The morph is one spring progress (out, swap at the transparent point, in) instead of AnimatePresence mode=wait, so interruptions collapse intermediate labels.
- Activation is the native composed click and the onClick property hook; the detent-click companion the row originally recorded was dropped on 2026-10-26 after the PC-6.1 review (a native event is never re-emitted under a custom name), and the row is corrected here.