Home

iOS Menu

This is the iOS context menu submenu - the thing that happens when you tap "Open With" in Files - rebuilt for the web with Motion and nothing else: no popover library, no second panel sliding in from the right.

Apple's trick is that the submenu isn't a new surface. The row you tap becomes the header of a card that expands in place: the chevron rotates from pointing right to pointing down, the header keeps the row's exact height and padding, and the items unfold underneath it. Behind it the menu dims and shrinks slightly, pivoting on the row you tapped, so the two cards stay left-aligned and the header sits exactly where the row was. Motion carries all of it on two springs - one on height to open, a bounce-free one to collapse back - and the rest is plain CSS.

The anchor row never leaves the layout, and that's the part I got wrong first. My first version expanded the card inline, in the list, and every row below it got shoved down as it grew - which is exactly what iOS never does. The card has to be an absolutely positioned overlay at the row's measured position, floating above the dimmed list, so nothing underneath moves a pixel.

Two things worth knowing if you try the same morph. The header label gains a little weight as the card opens, and that has to be an animated number rather than a CSS swap - swapping snaps the text the instant the card mounts, which against a still-expanding card reads as a glitch. It only works because Geist is variable: the browser renders the fractional weights Motion writes out mid-flight instead of rounding to the nearest cut. And Motion owns transform-origin, rebuilding it from its own originX/originY props, so setting it in plain CSS gets silently overwritten and the card pivots around its center instead - a few pixels off its row, which is the one thing this effect can't survive.

The glass is backdrop-filter: blur(40px) saturate(180%) under a translucent fill, with a 1px inset rim highlight for the edge - the saturate is what makes the colors behind the menu glow through instead of graying out. Do not hand-write the -webkit- prefix next to it. This project builds CSS through Lightning CSS, and pairing the prefixed property with the standard one made it drop both from the output - the rule shipped with no blur at all, in every browser.

Escape, clicking outside, and tapping the dimmed parent all collapse it, and MotionConfig with reducedMotion={'user'} turns the springs off for anyone who has asked their OS for less motion - the submenu then simply appears and disappears in place.