morphcard

API reference

createMorph, its options and the instance it returns, the useMorph hook, and the data attributes.

createMorph(options)

import { createMorph } from "morphcard";

const morph: Morph = createMorph(options: MorphOptions);

Creates a controller for one sheet. It sets hidden on the sheet and scrim straight away and listens for Escape and clicks on [data-morph-close]. Throws a TypeError when sheet is missing or when background or scrim contains the sheet.

Options

OptionTypeDefault
sheetHTMLElementrequiredThe detail surface. position: fixed or absolute, full size.
backgroundHTMLElement | nullnullWhat recedes behind the sheet. Scaled and made inert while open.
scrimHTMLElement | nullnullDims the background. Its opacity animates 0 to 1.
prepare(card) => void | Promise<void>Fill the sheet for this card. Runs before measuring. card is null for a deep link.
onStateChange(state, card) => voidCalled on every state change.
duration{ open?: number; close?: number }{ open: 400, close: 300 }Milliseconds.
easing{ surface?: string; content?: string }see belowCSS easing strings.
staggernumber45Delay between data-morph-stagger blocks, in ms.
backgroundScalenumber | false0.96Scale of the background while open. false keeps it still.
reducedMotion"system" | boolean"system""system" follows prefers-reduced-motion. true forces fades.
timeScalenumber1Multiplies every duration and delay. 4 plays four times slower.
closeOnEscapebooleantrue
restoreScrollbooleantruePut the list's scroll position back before measuring on close.
sharedstring[]all keysKeys allowed to fly. Other marked elements fade in place.
radiusnumbercomputedCorner radius of the card in px. Read from the card's border-radius when omitted.
scrollerHTMLElement | Windownearest scrolling ancestorThe list's scroll container, for restoreScroll.

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
}

The Morph instance

interface Morph {
  open(card?: HTMLElement | null): Promise<boolean>;
  close(options?: { to?: HTMLElement | null }): Promise<boolean>;
  readonly state: "closed" | "opening" | "open" | "closing";
  readonly card: HTMLElement | null;
  readonly plan: MorphPlan | null;
  setOptions(options: Partial<MorphOptions>): void;
  destroy(): void;
}

open(card) opens the sheet from card. Pass null to open without a flight, as for a deep link: the sheet fades in. The promise resolves true when the sheet is open and false when another call took over first. Calling it while the same card is closing turns the close around.

close(options) closes back to the card it opened from. Pass { to: element } when the list re-rendered and the card is a new element, or { to: null } when there is nothing to return to (the sheet fades out). Resolves true once closed.

state is also written to the sheet as data-morph-state="opening" | "open" | "closing", and removed when closed, so CSS can react to it.

plan describes the latest transition:

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";

setOptions(options) changes timing and callbacks for the next transition. The three elements cannot change; create a new instance for that.

destroy() stops any transition, removes every attribute and style the library set, hides the sheet and removes the listeners.

Exported constants

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 references; pass options to change timing.

useMorph(options)

import { useMorph } from "morphcard/react";

const morph = useMorph(options?: Omit<MorphOptions, "sheet" | "background" | "scrim" | "prepare">);

Returns:

Field
sheetRef, backgroundRef, scrimRefCallback refs for the three elements. The instance is created once the sheet is mounted.
stateThe morph state as React state, so the component re-renders on change.
open(card, update?)update runs inside flushSync before measuring. Put the setState that fills the sheet there.
close(options?)Same as Morph.close.
instanceThe underlying Morph, or null before the sheet mounts.

Options are applied with setOptions after every render, so they can change freely. The instance is destroyed on unmount.

Data attributes

AttributeSet by
data-morph="key"youPairs a card element with a sheet element.
data-morph-mode="box" | "text"youHow a pair scales: by width (box) or by font size (text).
data-morph-staggeryouStaggered content block in the sheet.
data-morph-dockyouBar that slides up from the bottom.
data-morph-closeyouCloses on click.
data-morph-focusyouFocus target in the sheet after open, or in the card after close.
data-morph-statelibraryOn the sheet while it is shown.
data-morph-ghostlibraryOn the temporary copy of the card inside the sheet during a transition. Removed at the end.

On this page