remocn logoremocn

Morphing Modal

A bento card lifts off the grid and blooms into a full-screen modal driven by a single heavy spring.

The most expensive transition in the kit. Five properties — top, left, width, height, and borderRadius — are all driven by one spring with high mass and low stiffness so the modal "blooms" instead of jumping. The source card's content fades out in the first third of the morph, the modal's content fades in (with a 20px translateY) only in the last third — that asymmetric handoff is what stops it from feeling like a cheap slideshow. Backdrop dims in lockstep with the spring.

Installation

$ pnpm dlx shadcn@latest add @remocn/morphing-modal

Usage

// src/Root.tsx
import { Composition } from "remotion";
import { MorphingModal } from "@/components/remocn/morphing-modal";

const MorphingModalScene = () => (
  <MorphingModal
    from={{ left: 460, top: 260, width: 360, height: 200 }}
    to={{ left: 80, top: 60, width: 1120, height: 600 }}
    morphAt={30}
  />
);

export const RemotionRoot = () => (
  <Composition
    id="MorphingModal"
    component={MorphingModalScene}
    durationInFrames={180}
    fps={30}
    width={1280}
    height={720}
  />
);

Props

PropTypeDefaultDescription
from
{ top, left, width, height }Source rect of the card before it blooms.
to
{ top, left, width, height }Target rect of the modal. Defaults to a near-fullscreen sheet.
borderRadiusFrom
number24Border radius of the source card in pixels.
borderRadiusTo
number0Border radius of the modal in pixels.
morphAt
number30Frame at which the morph spring fires.
background
string"#050505"Page background color.
cardColor
string"#0a0a0a"Card / modal surface color.
textColor
string"#fafafa"Heading color in source and modal.
mutedColor
string"#71717a"Body copy color.
sourceTitle
string"Compose video"Heading shown on the source card.
sourceBody
string"Click to start a new project"Body copy shown on the source card.
modalTitle
string"New project"Heading shown after the morph completes.
modalBody
stringBody copy shown after the morph completes.
source
ReactNodeOverride the default source card content.
modal
ReactNodeOverride the default modal content.
speed
number1Playback speed multiplier.
className
stringOptional className passed to the root container.

Notes

One spring, five properties

Animating each rect property from a single spring value is the trick — independent springs would drift out of phase and the morph would feel jelly-like in the worst way.

Asymmetric content fade

Source content fades over [0, 0.33], modal content arrives over [0.66, 1]. Don't overlap them — the empty middle third is what makes the bloom feel intentional.