Motion patterns Get motion.css

Patterns

Reorder

A list that sorts and filters by moving, not jumping. Rows that climb lift and pass in front, rows that drop sink behind, and changed numbers roll.

Open the demo alone
0.00s

Timeline loads with the demo.

A list that sorts and filters by moving. Rows that climb lift and pass in front of the rows they overtake, rows that drop slide behind, rows a filter removes fade where they stood while the list folds shut, and a number that changes rolls to its new value.

Use

<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="reorder/style.css">
<script src="reorder/reorder.js" defer></script>

<ol class="reorder" id="board">
  <li>...</li>
  <li>...</li>
</ol>
const board = document.getElementById("board");

// Do the change inside the callback: sort with appendChild, filter with el.hidden.
reorder.flip(board, () => {
  rows.sort(byPoints).forEach((row) => board.appendChild(row));
  rows.forEach((row) => { row.hidden = row.dataset.rank > 4; });
});

// A number that changes: 1 rolls up (the new value comes from below), -1 rolls down.
reorder.roll(rankCell, "3", 1);

flip is FLIP: it measures every child, runs your change, then plays each child from its old place to its new one with the Web Animations API. Call it again before the last one finishes and every row starts from where it is on screen, so a quick second click never jumps.

Options

PieceWhat it doesChange it with
Climbing rowLifts (scale 1.03) at the middle of its trip, passes in front, shows a soft shadow at the top of the arc--reorder-shadow, --reorder-edge
Dropping rowShrinks a little (0.985) and dims to 72% at the middle, passes behind
Trip length--dur-base x 0.7 plus 0.9 ms per pixel, capped at 1.5 x --dur-base, on --ease-out--dur-base
Removed rowLeaves the flow at once, fades and folds on --ease-in while the list shrinks to its new height
Added rowRises 12px into the gap a quarter beat after the others start
.rollOld value slides out, new one slides in, inside its own clipped box
Row backgroundRows need one, or the crossing shows through--reorder-bg (default Canvas)

Do

  • Keep the rows' height steady during the change. FLIP measures centres, so a row that changes height mid-trip will drift.
  • Say what changed in a polite live region ("Sorted by points. Sungai Rusa first."). The movement is for eyes only.
  • Keep it for lists of about 30 rows or fewer that the reader is looking at. Each row is one animation.

Do not

  • Put transform or opacity animations of your own on the rows: the moves use both and will replace them.
  • Reorder while the reader is dragging or typing in a row. Move the list after they let go.
  • Give .reorder a fixed height. It animates its own height when rows leave or arrive.

Reduced motion

flip runs the change and returns: rows land in their new place in one frame, removed rows disappear and added rows appear. roll swaps the number. The lift shadow is switched off.

style.css

/*
 * reorder: the CSS half of reorder.js. The script moves the rows (FLIP with the Web Animations API);
 * this file only sets the stacking that makes a reorder read as people overtaking each other.
 * A row that climbs passes in front of the rows it overtakes and lifts off the table on the way;
 * a row that drops slides behind them. Rows need an opaque background, or the crossing shows through.
 */
.reorder { position: relative; }
.reorder > * { position: relative; z-index: 1; background: var(--reorder-bg, Canvas); }
.reorder > [hidden] { display: none !important; } /* a row's own display: grid would otherwise beat [hidden] */
.reorder > .is-rising { z-index: 3; }
.reorder > .is-sinking { z-index: 0; }
/* while the list changes height, rows outside it are cut at its edge rather than spilling out */
.reorder.is-resizing { overflow: clip; }
/* a row a filter removes leaves the flow at once, so the others can close the gap while it fades */
.reorder > .is-leaving { position: absolute; z-index: 0; margin: 0; pointer-events: none; }

/* the lift: a soft shadow under a climbing row, shown only at the top of its arc */
.reorder > *::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 10px 24px -10px var(--reorder-shadow, rgb(16 24 40 / .35)), 0 0 0 1px var(--reorder-edge, rgb(16 24 40 / .08));
  opacity: 0;
  pointer-events: none;
}

/* roll: a number that changes rolls old out and new in, inside its own box */
.roll { display: inline-grid; overflow: hidden; vertical-align: bottom; }
.roll > * { grid-area: 1 / 1; }

@media (prefers-reduced-motion: reduce) {
  /* reorder.js skips every animation here; rows land in their new place in one frame */
  .reorder > *::after { display: none; }
}