ap an app icon on an iPhone and it doesn't just appear — it grows out of exactly where you tapped, its corners squaring off as it fills the screen. It's one of the most recognizable pieces of motion in software, and it's a genuinely hard thing to fake on the web, because the icon and the fullscreen view it becomes are usually two completely different DOM elements with no natural way to animate between them. Our iOS service page opens with that exact transition anyway.
Thetrickismeasuringwheretheiconactuallyis,thenanimatingasecondelementfromthatmeasuredrecttofillthescreen.Nothingmoves—theiconandthepanelarejustchoreographedtolineup.
Tap an icon
tap an app to open
This is the same underlying technique as the real page, scaled to a small stage: tapping an icon calls getBoundingClientRect() on the button you clicked and on the container it lives in, subtracts the two to get the icon's position relative to the stage, and stores that as a plain { top, left, width, height } rect — not a DOM reference, just four numbers.
typescript
const launch = (e: React.MouseEvent<HTMLButtonElement>) => { const el = e.currentTarget.getBoundingClientRect(); const host = stageRef.current!.getBoundingClientRect(); setLaunching({ top: el.top - host.top, left: el.left - host.left, width: el.width, height: el.height, }); setOpen(true); };
A second element inherits that rect as its starting point
tsx
<motion.div initial={{ ...launching, borderRadius: '22%' }} animate={{ top: 0, left: 0, width: '100%', height: '100%', borderRadius: 0 }} transition={{ duration: 0.55, ease: [0.32, 0.72, 0, 1] }} />
The icon you tapped never moves at all — it's a completely unrelated element that fades out under a blur. A brand-new motion.div mounts with initial values equal to the icon's measured rect, exactly overlapping it for one frame, then animates to fill the stage. Because the starting position, size, and corner radius all match the icon precisely, the illusion reads as one object growing rather than two objects that happen to be choreographed. This is the shared-element / FLIP pattern (First, Last, Invert, Play) — measure before, measure after, animate the delta — done by hand instead of through a dedicated layout-animation library.
Why not Framer Motion's layoutId
Framer Motion actually ships a purpose-built tool for exactly this — give two elements the same layoutId and it handles the FLIP measuring automatically, even across a conditional mount/unmount. We didn't reach for it here because the source and destination aren't really "the same element in two states" the way layoutId assumes — the icon stays a small square glyph forever, and the destination is a fully different fullscreen layout with its own content fading in on a delay. Manual rects gave more control over exactly when the icon fades versus when the panel's content arrives, at the cost of writing the coordinate math by hand instead of trusting a shared ID to infer it.