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-modalUsage
// 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
| Prop | Type | Default | Description |
|---|---|---|---|
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 | number | 24 | Border radius of the source card in pixels. |
borderRadiusTo | number | 0 | Border radius of the modal in pixels. |
morphAt | number | 30 | Frame 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 | string | — | Body copy shown after the morph completes. |
source | ReactNode | — | Override the default source card content. |
modal | ReactNode | — | Override the default modal content. |
speed | number | 1 | Playback speed multiplier. |
className | string | — | Optional className passed to the root container. |
Notes
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.
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.