# Choreography

> Make several things move together with intent, using stagger, travelling elements, clip-path reveals and grouped hover.

Module: Motion · Level: intermediate · 9 min · https://school.agents.org.in/design/motion/choreography

Slide the delay to 150ms and press replay. The last row arrives so late it feels like the search is still loading. Now try 40ms.

*[Interactive demo: Search results arriving in a cascade. Watch the total, not the per-row number.]*

A stagger (Playing the same entrance on several items one after another, a few tens of milliseconds apart, so a list arrives as a quick wave) is the same entrance played on several items, each a little after the last. Done well it shows order and pulls the eye down the list. Done badly it makes a fast app feel slow.

## Stagger: small gaps, short cascade

- **30–80ms between items.** Under 30 looks like everything at once with extra steps. Over 80 starts to feel like loading.
- **Keep the whole cascade under about 300ms.** Past around a dozen items, cap it: items past the cap arrive with the last staggered one.
- **Only on first appearance.** The first search result list can cascade; refining the search, sorting or filtering should not re-stagger. Update in place.
- **Only when order means something.** A stagger implies a reading order. Identical items in arbitrary order shouldn't pretend to have one.
- **Exit together.** If you must stagger an exit, reverse it; usually a faster, single fade is better.

For a page or hero section, split it into heading, text and button and offset them ~90–100ms, each entering with opacity, `translateY(8px)` and a 2px blur. And remember that **first paint is not an entrance**: what's already on screen when the page loads should simply be there.

```ts
const delay = (i: number) => Math.min(i, 12) * 0.05; // 50ms apart, capped at 12
```

## Fly, don't teleport

Benji Taylor's rule from the Family app: **if something is on screen before and after a change, it should travel, not disappear and reappear.** Click between tabs that are far apart in each mode.

*[Interactive demo: The active pill is one thing in two places. Let it move.]*

When the pill teleports, your eye has to search for where it went. When it flies, you watch it get there. The same goes for a list item moving to a new position, a card expanding into a full screen, or a stacked modal: the one underneath recedes slightly (scale 0.96, a little blur) because it's coming back, rather than vanishing.

The technique behind most of these is called FLIP (A technique for animating an element to a new position: measure First and Last positions, Invert the difference with a transform, then Play it back to zero): measure where the element is (First), let it move to its new spot (Last), shift it back with a transform so it *looks* unmoved (Invert), then transition the transform away (Play). Motion's `layoutId` does this for you. Crossfading is still right when two things are genuinely different content; forcing travel between unrelated cards is decoration.

*(The pill in this demo moves by clip-path, not width. More on that next.)*

## clip-path: the underrated one

clip-path (A CSS property that cuts away part of an element, like a mask. It animates cheaply and smoothly, which makes it great for reveals, wipes and sliding highlights) cuts away part of an element, and it animates smoothly and cheaply. `inset(top right bottom left)` clips from each edge; both ends of a transition must use the same shape function. Hold this button down.

*[Interactive demo: Hold to delete. Let go early and it snaps back.]*

```css
.fill { clip-path: inset(0 100% 0 0); transition: clip-path 200ms var(--ease-out); }   /* release: fast */
.button:active .fill { clip-path: inset(0 0 0 0); transition: clip-path 2s linear; } /* hold: slow */
```

The press is slow and `linear` because it's progress and you're deciding; the release answers in 200ms. Put a single duration on the base rule and an aborted hold crawls back for two whole seconds.

Other patterns from Emil Kowalski's notes:

- **Tabs whose text flips color exactly under a sliding pill**: two text layers, the top one clipped to the pill.
- **Image reveals on scroll**: `inset(20% 0 20% 0)` to `inset(0)`, like a letterbox opening.
- **Before/after sliders**: the "after" image clipped by a CSS variable that follows the pointer.
- **Directional swaps**: one panel clips out to the left as the next clips in from the right, with no layout shift.

Combine it with rounded corners using `inset(… round 12px)` rather than `border-radius`, which double-clips in Safari.

## Grouped hover: the falloff

Hover or tap a face in this stack. Its neighbours lift too, a little less with each step away.

*[Interactive demo: lift × falloff^distance. Slide the falloff and compare.]*

From Jakub Antalik's avatar group: `shift = lift × falloff^distance`, with the same idea for scale. With a lift of −8px and a falloff of 0.45, the neighbour moves −3.6px, the next −1.6px, and by four away it's under a pixel. **0.4–0.5 is the sweet spot**: 0.8 drags the whole row along, 0.2 is just one face. The lift uses ease-out; the return uses a slight overshoot, so it settles with life. Skip it on dense data tables and lists over ~20 items, and note it can't fire on touch-only screens without a tap.

**Choreograph a list and tabs (paste into your agent):**

```text
Stagger the search results only on their first appearance: 40ms between rows, capped so the whole cascade stays under 300ms, opacity plus translateY(8px), ease-out. Don't re-stagger on filter or sort. Make the active tab indicator one persistent element that slides between tabs (FLIP, layoutId, or clip-path inset on a full-width pill) with cubic-bezier(0.77, 0, 0.175, 1) over ~300ms, instead of re-rendering under each tab.
```

**Check yourself:** A results list staggers 120ms per row across 15 rows every time the user changes a filter. What's wrong?

- [ ] Nothing; stagger makes lists feel polished
- [ ] Only the curve; switch to a spring
- [x] The gap is too long, the cascade far over 300ms, and it re-staggers on every filter change
- [ ] It should stagger from the bottom up

*Why:* 15 rows at 120ms is a 1.7s cascade, the gap is past 80ms, and stagger belongs on first appearance only. Refinements should update in place.

## In the skill

- https://school.agents.org.in/skill/motion/stagger-choreography.md
- https://school.agents.org.in/skill/motion/fly-not-teleport.md
- https://school.agents.org.in/skill/motion/clip-path-tricks.md
- https://school.agents.org.in/skill/motion/distance-falloff-propagation.md
