Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 10.2Intermediate ~25 min · shot 2 of 6

Reordering and Filtering Lists

In one line

FLIP for many items: read every box, change the list once, and animate only what moved. Items that leave fade out as copies, items that arrive fade in, and a child inside a resized box needs a counter-scale.

0/6 · 0%

Think of it like this

A dance teacher says "new formation" and fifteen dancers change places. Nobody teleports and nobody walks through anyone. The teacher knows each dancer's starting spot and end spot and tells each one only the path between them. Dancers who stay put get no instructions.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

counter-scale
A scale on a child that is the inverse of its parent's scale, so the child keeps its size while the parent is scaled.
fade-out copy
A clone of a removed element placed where it was, so something can fade out when the real element is already gone.
empty state
What the page says when a list has no items, so the user knows it is not broken.
batch read
Reading all the boxes before changing anything, which costs one layout instead of one per element.

Step by step

01Filter twelve chips

In the list example press Blue. Four chips stay and slide left; eight fade out where they were. Pause right after pressing and the timeline shows 12 tracks: 4 slides and 8 fades.

02Press the same filter again

Press Blue twice in a row. Still 12 tracks. The second press found nothing that moved.

03Interrupt it

Set the speed to 0.25×, press Blue, and after 100 ms press All. Chips that were sliding left turn round from where they are. Nothing jumps.

04Empty and huge

Press None: the status says No items match this filter. and all twelve fade. Press 200 items, then Blue: 200 animations (67 slides and 133 fade-out copies). That is a lot of layers: the first pro note below says what to do.

05Resize a box

In the scale example press Grow the panel, then pause at 400 ms. The label shrinks to 0.7 of its size. Tick the counter-scale box and repeat: the label is now at 1.75, too big.

Try it yourself

  1. 1

    Count the tracks for each filter

    Pause, press Orange, and count the tracks: 11, which is 3 slides and 8 fade-out copies (the first orange chip is already in the first place, so it does not move). Blue gives 12: all four blue chips slide.

  2. 2

    Read the counter-scale

    In the scale example, tick the box, press Grow, pause at 400 ms and click the label's track. The panel is at scale(0.7, 0.72) and the label at scale(1.75, 1.625). A perfect counter-scale would be 1.43 and 1.38: the straight line from 2.5 to 1 passes above the true inverse.

  3. 3

    Rapid Blue, Ink, Blue

    At 0.1×, press Blue, Ink and Blue quickly. Each press cancels the slides still playing and starts new ones from where each chip is on screen. The fade-out copies already placed keep fading.

Code & diagrams

Filtering a list with flip()

This is the project's flip.js (the same code as the build). Try Blue then Ink 100 ms apart, None, All, and 200 items.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

items: 12
Feel itFilter the photos

Press the colour buttons in each version. Which one helps you keep track of the chips?

A Instant

Press Run to see the page.

B With FLIP

Press Run to see the page.
Play both, then choose:
Counter-scaling a child

Grow the panel with and without the tick. Pause at 400 ms and compare the label's size. The console prints the panel's starting scale.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Skip the cancel of the old animation

In the project's flip.js, delete the line for (const el of items) running.get(el)?.cancel();.

terminal
$ Press Blue, then All 100 ms later, and read the left edge of the fifth chip (a blue one) just before and just after the second click
── what you'll see ──
66.0 px before and 43.0 px after: the chip jumps 23 px. With the line kept, both readings are 66.0 px.

Break #2

Hide with opacity instead of hidden

In the list page's apply(), replace chip.hidden = ... with chip.style.opacity = ... ? 0 : 1 (use the same condition).

terminal
$ Press Blue
── what you'll see ──
The unwanted chips disappear at once: no tracks run (`document.getAnimations().length` is 0), the chips that stay do not move, and the gaps stay.

Myth vs fact

Myth

Counter-scaling makes the child look perfectly steady.

Fact

It is an approximation: the child wobbles a little in the middle of the move. GSAP Flip and Motion sample many keyframes to make it close to exact.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Fade-out copies are DOM nodes and may be layers: filtering 200 chips to Blue makes 133 of them, for 200 ms. For very long lists animate only the elements inside the viewport (check their box against innerHeight), and let the rest change at once.

  • ▸

    Everything below the list moves when the list changes height. The build passes the next section to flip() too, so the page glides instead of jumping.

  • ▸

    Change the layout with change() only. If it awaits (a fetch) the First box is stale; read First, await, change, then measure Last.

Remember this

  1. 1

    The problem this solves: a filter or sort changes a whole list at once. Without motion the grid just rearranges, and the eye loses track of which photo went where (Lumen's testers thought the photos had vanished). With FLIP each survivor slides to its new place, and the eye follows it.

  2. 2

    One function for every list. flip(elements, change, options) does First, runs your change(), does Last, and plays every element that moved. It never decides what changes: your callback sets hidden, reorders nodes, or switches a class. Pass every element that might move, because reading 200 boxes in a batch is one layout, and skipped ones cost nothing.

  3. 3

    Elements that leave. A filtered-out element becomes display: none: it has no box, so there is nothing to transform. The standard answer is a fixed-position copy placed at its old box that fades out and removes itself (the copy is aria-hidden and inert). The real element is gone at once, so the others can move into its place immediately.

  4. 4

    Elements that arrive. Before the change they had no box, so they have no First. They enter with CSS: @starting-style gives them an opacity and scale to start from (Topic 10.3). FLIP leaves them alone.

  5. 5

    Scale distortion. If an element also changes size, you can FLIP the size with scale. But the scale applies to everything inside: text shrinks, borders and rounded corners squash. A counter-scale on the child (scale(1/sx, 1/sy) to none) cancels it. It is only approximate: the parent's scale is interpolated in a straight line but its inverse is not, so halfway through, the child is about 22% too big in the example below.

  6. 6

    Interruption. The user clicks a second filter while the first is still sliding. getBoundingClientRect() includes the running transform, so First is where the element is on screen right now. The old animation is cancelled before Last is measured, and the new one starts from the visible position. Nothing jumps.

  7. 7

    The same filter twice changes nothing: the second click should do nothing at all. No matches needs an empty state in words ("No items match this filter.") because a grid of nothing looks broken. And with reduced motion, flip() skips the travel and fades leaving items quickly.

Explain it without notes

01

How does flip() treat an element that is removed from the layout, one that arrives, and one that stays?

02

Why does the second click on a filter not break the animation that is still running?

Practice

01

Add a "Sort" button to the list example that orders the chips by colour. What goes inside change()?

02

The panel example grows from 80 by 40 to 200 by 90. What are sx and sy, and what should the label's first scale be?

Trade-offs

  • ↔

    Copies for leavers cost nodes and layers. A CSS exit (Topic 10.3) is simpler when the neighbours do not need to move into the gap.

Done when you can

  • You can run FLIP on a list with leaving and arriving items

  • You can explain why a second click does not jump

  • You can say when a counter-scale is needed and why it is approximate

  • You can handle the empty and 200-item cases