skip to content

A Vue 3 `<TransitionGroup>` list animates wrongly: removing a middle row fades out the last row, and shuffles jump instead of sliding; how do you diagnose it?

level: seniorimportance: should knowfreq 30%

answer

  1. which row actually left
  2. identity comes from the key
  3. does the move class transition transform
  4. leaving rows still take space

basics

~20 s

In Vue's TransitionGroup, the last row fading on a middle removal means index keys: Vue sees the last key disappear. Jumpy shuffles mean no detectable moves: index keys, no transform transition on the move class, inline elements, or leaving rows still in flow.

solid answer

~40 s

Start with the keys. With `:key="index"`, removing row 3 of 5 leaves keys 0 to 3 in place with shifted content and makes key 4 disappear, so `<TransitionGroup>` fades out the **last** row while the middle row's text changes instantly; a shuffle keeps every key at the same position, so nothing moves. Switch to the item's stable id. If moves still jump, check the move class: it must be `<name>-move` (or `moveClass`) with a CSS **transition** that includes `transform` or `all`; an opacity-only transition or a `@keyframes` animation disables moves. Then check layout: leaving items should get `position: absolute` in the leave-active class, otherwise neighbours jump after the fade, and `display: inline` items cannot be transformed. Finally confirm there is no dev warning `<TransitionGroup> children must be keyed.`

code

vue · 16 lines
vue
<template>
  <TransitionGroup name="list" tag="ul" class="tasks">
    <li v-for="task in tasks" :key="task.id" class="task">{{ task.title }}</li>
  </TransitionGroup>
</template>

<style scoped>
.tasks { position: relative; }
.task { display: block; }
.list-move,
.list-enter-active,
.list-leave-active { transition: all 0.4s ease; }
.list-enter-from,
.list-leave-to { opacity: 0; transform: translateX(24px); }
.list-leave-active { position: absolute; width: 100%; }
</style>

go deeper

for a junior

Know that list items in a TransitionGroup need a real id as key, not the loop index.

for a middle

Explain why index keys make the wrong row leave and shuffles look static, and what the move class needs in CSS.

for a senior

Run the diagnosis in order: warning, keys, move class transition, leaving items out of flow, display type; fix each with the mechanism it restores.

for a principal

Encode the working recipe in one shared list-animation component with lint rules against index keys, so teams stop rediscovering these failures.

## The report A sortable task list uses `<TransitionGroup>`. QA files two bugs: 1. Deleting the third of five tasks makes the **fifth** row fade out, while the third row's text silently changes. 2. Clicking "Shuffle" makes rows **teleport** to their new places instead of sliding. Both symptoms have a small set of causes, and they can be checked in order. ## Cause 1: index keys ```vue <TransitionGroup name="list" tag="ul"> <li v-for="(task, i) in tasks" :key="i">{{ task.title }}</li> </TransitionGroup> ``` `<TransitionGroup>` decides what entered, left and moved by comparing **keys** before and after the update. With index keys: | Action | Keys before | Keys after | What Vue concludes | |---|---|---|---| | Remove task 3 of 5 | 0,1,2,3,4 | 0,1,2,3 | Key 4 left; keys 2 and 3 got new text | | Shuffle 5 tasks | 0,1,2,3,4 | 0,1,2,3,4 | Nothing entered, left or moved | So the leave animation plays on the last row, and a shuffle is just text being patched in place. The fix is a stable identity: `:key="task.id"`. ## Cause 2: the move class does not qualify Moves are animated only when the move class carries a CSS transition on `transform`. Vue tests this before each move pass and silently skips moves when it fails. Check: - the class name matches: `list-move` for `name="list"`, `v-move` with no name, or the value of `moveClass`; - the rule is a **transition**, not a `@keyframes` animation; - the `transition-property` includes `transform` or `all`, not only `opacity`; - the rule is not overridden by a more specific selector elsewhere. ## Cause 3: leaving items still occupy space If removals are keyed correctly but the rest of the list still snaps into the gap at the **end** of the fade, the leaving item is still in the layout during its leave transition. Neighbours have nowhere to move until it is removed. Add `position: absolute` to the leave-active class so neighbours are measured in their final layout immediately and slide while the removed row fades. The container may need `position: relative` so the absolutely positioned row stays put visually. ## Cause 4: elements that cannot be transformed FLIP relies on `transform: translate(...)`. Non-replaced `display: inline` boxes ignore transforms, so rows rendered as bare `span`s never appear to move. Use `display: block`, `inline-block`, `flex` items or `grid` items. ## A diagnosis order that works 1. Open the console: `<TransitionGroup> children must be keyed.` means at least one child has no key at all. 2. Inspect the `v-for`: is the key the item's id, or its index? 3. In devtools, trigger a shuffle and watch one row: does it briefly get the move class? If not, the CSS check failed. 4. Read the computed `transition-property` of an element with the move class. 5. Check the leave-active rule for `position: absolute` and the rows' `display` value. ## Why each fix works - **Stable keys** give Vue the real before/after mapping, so removals target the right row and shuffles register as moves. - **A transform transition** is what turns Vue's inverse translate into visible motion. - **Out-of-flow leaving items** let neighbours reach final positions before the leave ends. - **Transformable display types** let the translate apply at all. ## Preventing regressions Once fixed, list animations tend to break again when someone refactors the markup or the styles. A few guards help: - Wrap the recipe in one shared list component that owns the `TransitionGroup`, the move rule and the leave rule, so feature code only supplies items and keys. - Lint for `:key` bound to the `v-for` index inside animated lists. - Keep the move and leave rules next to the component rather than in a global stylesheet where specificity changes can override them. - Add a visual or end-to-end check that removes a middle row and asserts the right text disappears. ## Summary Wrong-row animations are almost always keys. Jumping instead of sliding is keys, the move class's transition, leaving items in flow or inline elements. Check them in that order, and the list will animate the item the user actually touched.

  • After adding `position: absolute` to leaving rows, the fading row shrinks to its content width; why, and how do you fix it?
    An absolutely positioned element is sized by its content rather than stretching across its containing block, so a full-width row collapses while it fades. Give the leave-active class an explicit `width: 100%` (with the list container `position: relative`) or the row's measured width, so it keeps its shape until it is removed.
  • How can you confirm in the browser that Vue is applying move handling at all?
    Trigger a shuffle and watch one row in the element inspector. During a working move it briefly carries the move class (for example `list-move`) and an inline transform that is cleared immediately. If the class never appears, Vue's check found no transform transition for that class, so look at the CSS rather than the component.

saying these in an interview costs you the question

  • Index keys are fine in TransitionGroup because Vue re-reads the array order
  • An opacity transition on the move class is enough to animate reordering
  • TransitionGroup animates inline span rows the same as block rows
  • Leaving rows never affect how neighbours move
  • A missing key causes a runtime error rather than a dev warning