Pattern · 27
Shared bounds
共享边界过渡 · shared-boundsMake the detail view appear to grow from the exact card that was clicked.
An instant detail panel loses the link to the clicked card. FLIP measures the small card, lays out the destination, inverts it back over the source with a transform, then plays to identity. Measure the source again on close so it returns to the right place.
Formula
First = source.getBoundingClientRect() Last = detail.getBoundingClientRect() Invert: translate(First − Last) scale(First / Last) Play: transform → none
Parameters
| This page | Mature libraries | |
|---|---|---|
| duration | 450 ms | —Duration in this site's reference helper. |
| easing | cubic-bezier(.2,.8,.2,1) | — |
| source rect | remeasured on each open and close | — |
Doing it well
- Measure First before changing the layout, then measure Last. The inverted transform needs translation and both size ratios or the card changes size abruptly.Paul Lewis, “FLIP Your Animations”, 2015; www-motion/rewrite/lib/motion-kit.js:269–275
- Measure the current tile on close rather than reusing its old opening rect; the grid may have reflowed.Paul Lewis, “FLIP Your Animations”, 2015
- Keep the source tile identity so the detail view returns to that tile, not a default one.www-motion/rewrite/lib/motion-kit.js:269–275
In our cases
18 studies use shared bounds. momentum-slider, fluid-gallery, and morph link gallery items to detail views; continuous position and size clarify that relationship better than a fade alone.
Used in 18 cases
Reference implementation · basics.js
/**
* FLIP: measure First, change the layout to the Last state, Invert with a transform, Play to identity.
* Call with the element's rect from BEFORE the layout change.
*/
export function flip(el, first, { duration = 450, easing = 'cubic-bezier(.2,.8,.2,1)' } = {}) {
const last = el.getBoundingClientRect();
const dx = first.left - last.left, dy = first.top - last.top;
const sx = first.width / last.width, sy = first.height / last.height;
return el.animate([
{ transformOrigin: '0 0', transform: `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})` },
{ transformOrigin: '0 0', transform: 'none' },
], { duration, easing });
}