Spatial & directional motion

Where an element comes from matters as much as where it goes

## Transform-origin: the anchor point The default `center center` is correct for modals. Wrong for almost everything else. ```css /* Dropdown below a button */ .dropdown { transform-origin: top center; } /* Context menu from right-click point */ .context-menu { transform-origin: var(--click-x) var(--click-y); } /* Left-docked sidebar */ .sidebar { transform-origin: left center; } ``` } /> ## Directional tab indicators Motion's `layoutId` makes this straightforward: ```tsx { tabs.map((tab) => ( <button key={tab.id} onClick={() => setActive(tab.id)}> {tab.label} {active === tab.id && ( <motion.div layoutId="tab-indicator" className="absolute inset-x-0 bottom-0 h-0.5 bg-primary" transition={{ type: "spring", stiffness: 500, damping: 40 }} /> )} </button> )); } ``` } /> ## Continuity: shared elements across states When an element exists in both "before" and "after," morph it in place. A card's image expands to fill the detail hero. ## Emerge from trigger Overlays animate _from the element that opened them_. ## Scale starting values `scale: 0` looks cartoonish. `scale: 0.99` is imperceptible. Sweet spot: **0.85 to 0.95**. - **Modals**: `scale: 0.95` - **Popovers**: `scale: 0.9` - **Tooltips**: `scale: 0.85` - **Buttons (press)**: `scale: 0.97` ## Direction and user flow - **Forward** (drilling deeper): enters from right, exits left. - **Backward** (going up): enters from left, exits right. - **Vertical lists**: expanded panels push down, collapsed pull up. - **Mobile sheets**: enter from bottom, exit downward. } /> ## CSS entry animations with @starting-style ```css dialog[open] { opacity: 1; transform: translateY(0); transition: opacity 250ms ease-out, transform 250ms ease-out; @starting-style { opacity: 0; transform: translateY(12px); } } ``` For removal via `display: none`, use `transition-behavior: allow-discrete`: ```css .menu { display: block; opacity: 1; transition: opacity 200ms, display 200ms allow-discrete; @starting-style { opacity: 0; } } .menu[hidden] { display: none; opacity: 0; } ``` Ships in Chromium and Safari. Firefox is on track.