skip to content

In Vue 3, when a `<TransitionGroup>` list is shuffled, how does the move class slide items to their new positions, and what CSS does it need?

level: middleimportance: should knowfreq 38%

answer

  1. measure, then measure again
  2. offset back, then let go
  3. name-move or moveClass
  4. transform must be transitioned

basics

~20 s

Vue's TransitionGroup records each item's position before the update, measures again after, translates moved items back to their old spot, then adds the move class and clears the transform so a CSS transform transition slides them: the FLIP technique.

solid answer

~40 s

When the array is shuffled, Vue patches the DOM into the new order instantly. `<TransitionGroup>` makes that visible with **FLIP**: before the patch it records every item's position with `getBoundingClientRect()`; after the patch it measures again, and for each item whose position changed it sets an inverse `transform: translate(dx, dy)` with a zero transition duration, so the item appears at its old spot. It then forces a reflow, adds the **move class** and clears the transform, and the item transitions to its real position. The move class is `v-move`, `<name>-move` for a named group, or whatever `moveClass` says. It only works if that class declares a CSS `transition` covering `transform` (`transition: transform 0.4s` or `transition: all 0.4s`); Vue checks for this and skips move handling otherwise.

code

vue · 9 lines
vue
<template>
  <TransitionGroup tag="ul" move-class="reorder">
    <li v-for="row in rows" :key="row.id">{{ row.label }}</li>
  </TransitionGroup>
</template>

<style scoped>
.reorder { transition: transform 0.35s cubic-bezier(0.2, 0, 0, 1); }
</style>

go deeper

for a junior

Know that adding a name-move class with a transform transition makes reordered list items slide instead of jump.

for a middle

Walk through FLIP as Vue does it: measure before, measure after, invert with a translate, then add the move class and clear the transform.

for a senior

Name the conditions that silently disable moves: no transform in the move transition, keyframes instead of transitions, index keys, inline elements, leaving items still in flow.

for a principal

Weigh FLIP's per-update layout reads against list size and motion value, and set limits on where list move animations are worth their cost.

## The problem: reordering is instant Shuffling an array rendered with `v-for` makes Vue move the existing keyed DOM nodes into their new order in one synchronous patch. Enter and leave classes do not help: no item entered or left, they only changed places. Without extra work every item simply jumps. ```vue <script setup lang="ts"> import { ref } from 'vue' const cards = ref([1, 2, 3, 4, 5, 6, 7, 8, 9]) function shuffle() { const a = [...cards.value] for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)) ;[a[i], a[j]] = [a[j], a[i]] } cards.value = a } </script> <template> <button @click="shuffle">Shuffle</button> <TransitionGroup name="grid" tag="div" class="grid"> <div v-for="n in cards" :key="n" class="cell">{{ n }}</div> </TransitionGroup> </template> <style> .grid-move { transition: transform 0.5s ease; } </style> ``` Here the keys are the card values, which are unique and follow each card through the shuffle. ## FLIP, step by step `<TransitionGroup>` implements the **FLIP** technique (First, Last, Invert, Play): 1. **First**: when the group re-renders, before the DOM is patched, Vue records each existing item's position with `getBoundingClientRect()`. 2. **Last**: after the patch, in the group's updated hook, Vue measures every item again in its new place. 3. **Invert**: for each item whose left or top changed, Vue sets an inline `transform: translate(dx, dy)` equal to the old position minus the new one, with `transitionDuration` forced to `0s`. The item is now drawn exactly where it used to be. 4. **Play**: Vue forces a reflow, adds the **move class** and removes the inline transform and duration. Because the move class declares a transition on `transform`, the browser animates from the old offset back to zero, so the item slides into its new place. The class is removed when the `transitionend` event for `transform` fires. Reads and writes are batched in separate loops so the measuring does not cause repeated layout thrashing. ## The move class and its CSS requirement | Setup | Move class used | |---|---| | `<TransitionGroup>` with no `name` | `v-move` | | `name="grid"` | `grid-move` | | `move-class="slide-into-place"` | `slide-into-place` | Before doing any of the work, Vue checks whether an element with the move class applied has a CSS **transition** whose `transition-property` includes `transform` or `all`. If not, it skips move handling for that update. So: - `transition: transform 0.5s` or `transition: all 0.5s` on the move class: works; - `transition: opacity 0.5s` only: no move animation; - a `@keyframes` **animation** on the move class: no move animation, because the check looks for a transition. ## Making removals and insertions move smoothly When an item leaves, it keeps occupying layout until its leave transition ends, so its neighbours only jump into the gap afterwards. The documented fix is to take leaving items out of the layout flow: ```css .list-move, .list-enter-active, .list-leave-active { transition: all 0.5s ease; } .list-enter-from, .list-leave-to { opacity: 0; transform: translateX(30px); } .list-leave-active { position: absolute; } ``` With `position: absolute` on the leaving item, the remaining items are measured in their final layout straight away and the move class slides them into the gap while the leaving item fades. ## Caveats worth naming - Items need **stable keys**; with index keys a shuffle looks to Vue like content changing in place, so nothing is detected as moved. - The `transform` property does not apply to non-replaced `display: inline` boxes, so moving plain inline elements such as bare `span`s fails; make them `inline-block` or `block`. - An item hidden with `v-show` has no layout box and is skipped for move measurement. ## Cost and limits Every update of the group that reorders items costs two rounds of layout reads (before and after the patch) plus a forced reflow. For a few dozen items this is negligible; for hundreds of items reordered frequently it becomes visible work on each update. Two practical consequences: - Animate moves where they explain a change to the user, such as sorting or drag-and-drop, not on every background refresh. - If an update happens while a previous move is still running, Vue first finishes pending move and enter callbacks, then measures again, so rapid shuffles restart smoothly from the current layout rather than stacking transforms. ## Summary The move class is not an animation by itself: it is the switch that turns Vue's inverse-translate trick into visible motion. Give it a transition on `transform`, keep keys stable, and take leaving items out of flow so the rest of the list can slide instead of jump.

  • Why does a move class that uses a `@keyframes` animation produce no sliding?
    Before applying moves, `<TransitionGroup>` tests whether an element carrying the move class has a CSS transition whose `transition-property` includes `transform` or `all`. A keyframes animation is not a transition, so the check fails and Vue skips move handling for that update. Use `transition: transform <duration>` on the move class.
  • Why do the remaining items jump into place only after a removed item finishes fading out?
    The leaving item still occupies space in the layout until its leave transition ends, so the neighbours' new positions only exist afterwards. Setting `position: absolute` in the leave-active class takes it out of the flow immediately; Vue then measures the neighbours in their final positions and the move class slides them during the fade.

saying these in an interview costs you the question

  • Vue animates the move by tweening top and left in JavaScript each frame
  • Any transition on the move class works, including opacity-only transitions
  • A @keyframes animation on the move class drives the reordering
  • The move class is applied to every item on every update, moved or not
  • Shuffled items move smoothly even when keyed by array index