Open source · MIT · No dependencies

A card that grows into a detail screen and shrinks back.

morphcard animates the list-to-detail transition: the card becomes the screen, its title flies into the header, the content settles in, and back plays a shorter, calmer version. Try it on the right.

pnpm add github:pieralukasz/morphcard
state: closed

Slowed down

The same transition at a quarter speed

Open, hold, back. Recorded frame by frame, played four times slower.
  1. 0 to 400 ms. The sheet's clip grows from the card to the full screen. The route, company, reference and badge fly to the header. The list behind scales to 0.96 under a scrim.
  2. From 130 ms. Content blocks rise 10 px and fade in, 45 ms apart. The action bar slides up.
  3. Back, 300 ms. The heading gives way to the card's own text within the first 90 ms. The card's meta line fades back in over the second half, ending with the list.

What it does

The details that make it feel native

One surface that opens

The detail screen is clipped to the card's shape and the clip opens to full screen. Nothing is scaled, so text and images never stretch.

Shared text flies, the rest fades

Mark the route, name or badge with data-morph and they travel from the card to the header. Text that wraps differently crossfades instead of stretching.

Closing has its own timing

Back takes 300 ms against 400 ms to open. The big heading hands over to the card text early, so the last thing moving is the card itself.

Interruptible

Tap back while it opens and the same animations turn around from where they are. No jump, no second copy, no leftover styles.

Knows when not to fly

If the card is off screen or the target is missing, it crossfades instead of sending a lonely heading across an unrelated page.

Small and plain

TypeScript, no runtime dependencies, Web Animations API. An optional React hook. Reduced motion becomes an opacity-only fade.

Usage

Three elements and one call

import { createMorph } from "morphcard";

const morph = createMorph({
  sheet: document.querySelector(".sheet"),     // the detail screen
  background: document.querySelector(".list"), // recedes behind it
  scrim: document.querySelector(".scrim"),     // optional dimmer
  prepare: (card) => fillSheet(card.dataset.id),
});

list.addEventListener("click", (e) => {
  const card = e.target.closest(".card");
  if (card) morph.open(card);
});
// <button data-morph-close> inside the sheet closes it.

Put data-morph="title" on the card's title and on the sheet's heading, and they fly between the two. The getting started guide has the full markup and the React version.