Animating List Reorders with View Transitions
Part of View Transition Types & Nested Groups in Modern View Transitions & Scroll APIs.
The problem
A task board lets users sort by due date, filter by assignee and drag cards to reorder. Without animation, every change reshuffles the grid instantly and users lose track of the card they were looking at. The existing FLIP implementation measures every card before and after, applies inverse transforms and plays them back — about eighty lines of code that break when cards change size, when the grid wraps differently, or when filtered-out cards need to fade instead of move.
View transitions can do the same job with the browser doing the measuring.
Root cause analysis: snapshots paired by name
A same-document view transition captures named elements before and after a DOM update, then animates each pair’s group from its old box to its new box. For a reorder, that is exactly the motion needed: each card’s group travels from its old grid position to its new one, and resizes if the card’s size changed.
Three kinds of item appear. Items present in both states become moved groups, containing both an old and a new image. Items only in the old state are removed — their group contains only ::view-transition-old. Items only in the new state are added — only ::view-transition-new. The :only-child pseudo-class on those images distinguishes the three without script.
Names must be unique per capture. Every card needs its own name. view-transition-name: match-element provides this automatically for same-document updates, as described in automatic view transition names. Where it is unsupported, a name derived from the item’s id and set once in markup works just as well.
The update must be synchronous. The callback should perform the DOM reorder before it returns, or return a promise that resolves after the framework commits the new order.
Everything else is a snapshot. While the transition runs, the page is a set of images. Interaction is blocked until it finishes, so reorder transitions should be short — a few hundred milliseconds.
FLIP still has a place. View transitions animate snapshots, so live content inside cards — a playing video, a running progress bar — freezes during the transition. FLIP animates the live elements. For drag-and-drop, where the dragged card must stay live under the pointer, FLIP or direct transforms are usually better; view transitions suit discrete sort and filter actions.
Step-by-step resolution
Production code pattern
.task-card {
view-transition-name: match-element;
view-transition-class: task;
}
/* Moved cards. */
::view-transition-group(.task) {
animation-duration: var(--duration-400);
animation-timing-function: var(--ease-standard);
}
/* Removed cards: old image only. */
::view-transition-old(.task):only-child {
animation: task-out var(--motion-exit-duration) var(--motion-exit-easing) both;
}
/* Added cards: new image only. */
::view-transition-new(.task):only-child {
animation: task-in var(--motion-enter-duration) var(--motion-enter-easing) both;
}
@keyframes task-out { to { opacity: 0; scale: 0.9; } }
@keyframes task-in { from { opacity: 0; scale: 0.9; } }
/* Long boards: only visible cards take part as named groups. */
.board--large .task-card:not(.is-near-viewport) {
view-transition-name: none;
}
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
function applyView(nextState) {
const update = () => renderBoard(nextState); // synchronous DOM reorder
if (!document.startViewTransition || reduce.matches) return update();
document.startViewTransition(update);
}
sortSelect.addEventListener('change', () => applyView({ ...state, sort: sortSelect.value }));
assigneeFilter.addEventListener('change', () => applyView({ ...state, assignee: assigneeFilter.value }));
@media (prefers-reduced-motion: reduce) {
::view-transition-group(.task),
::view-transition-old(.task),
::view-transition-new(.task) { animation: none; }
}
Rendering Impact: composite for the group movement and fades, with one old and one new texture per named card. Limiting names to cards near the viewport keeps texture memory proportional to what is on screen, not to the length of the board.
The .is-near-viewport class comes from an IntersectionObserver with a generous root margin. Cards far off screen join the root snapshot, which cross-fades; since they are not visible, nobody sees that they did not slide.
Scroll position and the reordered item
Sorting can move the card the user was looking at far down the list. A view transition animates it there, but if it leaves the viewport the user still loses it. After the update, consider scrolling the focused or previously visible item back into view inside the same callback, so the new snapshot already shows it in place, and keep focus on it for keyboard users. If the list is long, announce the new order briefly in a live region (“Sorted by due date, 42 tasks”) so screen reader users know the change happened, since the animation itself conveys nothing to them — see animating ARIA live region updates.
Verification checklist
Constraints and trade-offs
- Live content inside cards freezes during the transition.
- Interaction is blocked for the transition’s duration; keep it short.
- Very large numbers of named elements cost snapshot memory and capture time.
- Drag-and-drop generally needs live transforms rather than snapshot transitions.
- Cards that change size significantly may distort text unless snapshot sizing is controlled.
Frequently asked questions
Can view transitions animate a list sort?
Yes. Give each item a unique view-transition-name and perform the sort inside document.startViewTransition; each item’s group moves from its old position to its new one.
How do I animate items that are removed by a filter?
Their group has only an old image. Target ::view-transition-old(.class):only-child with an exit animation.
Should I use FLIP or view transitions for reordering?
View transitions for discrete sort and filter actions; FLIP when items must stay live during the motion, such as during dragging.
How many list items can I name?
Name only those in or near the viewport. Each named item costs two snapshot textures, so naming hundreds of off-screen items wastes memory.
Related
- View Transition Types & Nested Groups — the parent topic
- FLIP Technique for Layout Animations — the live-element alternative
- Exit Animations for Removed DOM Elements — other ways to animate removals