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/morphcardclosedSlowed down
The same transition at a quarter speed
- 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.
- From 130 ms. Content blocks rise 10 px and fade in, 45 ms apart. The action bar slides up.
- 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.