The useMorph hook, its options, what it returns, the exported constants and the data attributes.
import { useMorph } from "react-morphcard";
const morph = useMorph<Item>(options?: UseMorphOptions);One hook per sheet. Item is the type of morph.item and defaults to unknown. The hook works in any modern browser, in installed PWAs and in WebViews. It does not work in React Native.
The hook creates its animation engine once the sheet element is mounted and destroys it on unmount. It is safe to call during server rendering: it touches window and document only in effects and event handlers. The package's built file starts with "use client", so a Next.js App Router page can import it directly.
All options are optional. They are applied after every render and again at the start of each open and close, so a change takes effect on the next transition. An option you remove goes back to its default.
| Option | Type | Default | |
|---|---|---|---|
duration | { open?: number; close?: number } | { open: 400, close: 300 } | Milliseconds. |
easing | { surface?: string; content?: string } | see below | CSS easing strings. |
stagger | number | 45 | Delay between data-morph-stagger blocks, in ms. |
backgroundScale | number | false | 0.96 | Scale of the background while open. false keeps it still. |
reducedMotion | "system" | boolean | "system" | "system" follows prefers-reduced-motion. true forces fades. |
timeScale | number | 1 | Multiplies every duration and delay. 4 plays four times slower. |
closeOnEscape | boolean | true | |
restoreScroll | boolean | true | Put the list's scroll position back before measuring on close. |
shared | string[] | all keys | Keys allowed to fly. Other marked elements fade in place. |
radius | number | computed | Corner radius of the card in px. Read from the card's border-radius when omitted. |
scroller | HTMLElement | Window | nearest scrolling ancestor | The list's scroll container, for restoreScroll. |
onStateChange | (state, card) => void | Called on every state change. The latest function is always used. |
Default easings:
easing: {
surface: "cubic-bezier(0.32, 0.72, 0, 1)", // clip, flights, background
content: "cubic-bezier(0.23, 1, 0.32, 1)", // staggered content, dock
}interface UseMorph<Item> {
sheetRef: (el: HTMLElement | null) => void;
backgroundRef: (el: HTMLElement | null) => void;
scrimRef: (el: HTMLElement | null) => void;
cardRef: (key: string | number) => (el: HTMLElement | null) => void;
state: "closed" | "opening" | "open" | "closing";
item: Item | null;
open(target: HTMLElement | string | number | { key?; card?; item? } | null, update?: () => void): Promise<boolean>;
close(options?: { to?: HTMLElement | string | number | null }): Promise<boolean>;
readonly instance: { state; card; plan } | null;
}sheetRef, backgroundRef, scrimRef are callback refs for the three elements. The sheet is required. The background and scrim are optional. None of them may contain another. Keep the sheet mounted: the hook shows and hides it with the hidden attribute.
cardRef(key) returns a callback ref for the card with this key. It returns the same function for the same key on every render. Register cards this way when you want to open or close by key instead of passing elements.
state is React state, so the component re-renders when it changes. It is also written to the sheet as data-morph-state="opening" | "open" | "closing", and removed when closed, so CSS can react to it.
item is the item passed to the latest open. It is set inside flushSync before anything is measured, so the sheet already shows it when the flight is planned. It goes back to null only after the close has finished, so the sheet keeps its content during the whole flight back.
open(target, update?) opens the sheet. target can be:
morph.open(event.currentTarget);cardRef, as in morph.open(item.id);{ key, card, item }. card wins over key when both are given. item becomes morph.item;null, to open without a flight, as for a deep link. The sheet fades in.update runs inside flushSync before measuring. Put a state update that fills the sheet there if you keep the selected item in your own state. The promise resolves true when the sheet is open and false when another call took over first. Opening the card that is closing turns the close around. Opening another card while one closes finishes that close at once.
close(options?) closes back to the card it opened from. When that card was opened by key and the list has rendered a new element for the key since, the flight goes to the new element. Pass { to } to choose another card: an element, a registered key, or null when there is nothing to return to (the sheet then fades out). Resolves true once closed. Escape, [data-morph-close] elements and a click on the scrim call it for you.
instance is a read-only view of the engine once the sheet is mounted, and null before. Its plan describes the latest transition and is useful when a flight did not happen:
interface MorphPlan {
direction: "open" | "close";
choreography: "morph" | "fade";
reason?: SkipReason; // why it faded instead of morphing
reduced: boolean;
backgroundScaled: boolean;
pairs: { key: string; mode: "scale" | "crossfade" | "skip"; reason?: SkipReason }[];
}
type SkipReason =
| "reduced-motion"
| "no-card"
| "card-offscreen"
| "sheet-hidden"
| "not-shared"
| "missing-card-element"
| "missing-sheet-element"
| "sheet-element-offscreen";instance.card is the card element the sheet belongs to, or null.
open, close, cardRef and the three element refs keep their identity across renders. The object returned by the hook changes when state or item changes.
defaults holds the default timing. choreography holds the fractions of the duration used for each part of the transition (see Anatomy). Both are read-only. Pass options to change timing.
The package exports UseMorph, UseMorphOptions, UseMorphCloseOptions, MorphTarget, MorphKey, MorphInstance, MorphState, MorphPlan, MorphTiming, PairReport and SkipReason.
| Attribute | Set by | |
|---|---|---|
data-morph="key" | you | Pairs a card element with a sheet element. |
data-morph-mode="box" | "text" | you | How a pair scales: by width (box) or by font size (text). |
data-morph-stagger | you | Staggered content block in the sheet. |
data-morph-dock | you | Bar that slides up from the bottom. |
data-morph-close | you | Closes on click. |
data-morph-focus | you | Focus target in the sheet after open, or in the card after close. |
data-morph-state | library | On the sheet while it is shown. |
data-morph-ghost | library | On the temporary copy of the card inside the sheet during a transition. Removed at the end. |
Made by Lucas Piera