skip to content

In Vue 3, how does `<TransitionGroup>` differ from `<Transition>`, and why does it require keys but offer no `mode` prop?

level: juniorimportance: should knowfreq 46%

answer

  1. many elements, not one
  2. classes land on each item
  3. no wrapper unless asked
  4. nothing to alternate between

basics

~20 s

Vue's TransitionGroup animates many list items at once: each item gets the enter/leave classes, moved items get a move class, and no wrapper renders unless tag is set. Keys identify which items entered, left or moved; mode is absent because nothing alternates.

solid answer

~40 s

`<Transition>` animates **one** element or component appearing, disappearing or being swapped; `<TransitionGroup>` animates the insertion, removal and reordering of **many** items rendered with `v-for`. It accepts the same props, CSS classes and JavaScript hooks as `<Transition>`, with four differences: the classes are applied to **each item**, not to a container; it adds a **move** class (`v-move`, or `<name>-move`, or `moveClass`) for items whose position changed; it renders **no wrapper element** unless you pass `tag`; and it has **no `mode`**, because modes sequence one leaving element against one entering element, and in a list many items enter and leave independently. Every child must have a unique `key`, because the key is how Vue tells which item entered, which left and which moved; unkeyed children trigger the dev warning `<TransitionGroup> children must be keyed.`

code

vue · 24 lines
vue
<script setup lang="ts">
import { ref } from 'vue'

const items = ref([{ id: 1, text: 'Write tests' }, { id: 2, text: 'Ship' }])
let nextId = 3
const add = () => items.value.unshift({ id: nextId++, text: 'New task' })
const remove = (id: number) => { items.value = items.value.filter((i) => i.id !== id) }
</script>

<template>
  <button @click="add">Add</button>
  <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item.id" @click="remove(item.id)">
      {{ item.text }}
    </li>
  </TransitionGroup>
</template>

<style scoped>
.list-enter-active,
.list-leave-active { transition: all 0.3s ease; }
.list-enter-from,
.list-leave-to { opacity: 0; transform: translateX(24px); }
</style>

go deeper

for a junior

Remember the split: Transition for one element, TransitionGroup for a keyed v-for list, with classes applied to each item.

for a middle

Explain the differences precisely: no wrapper unless tag is set, a move class for repositioned items, mandatory keys, and why mode has no meaning for a list.

for a senior

Spot upgrade and identity pitfalls: Vue 2 code relying on the implicit span, and index keys that make removals and shuffles animate the wrong items.

for a principal

Standardise list animation in a shared wrapper component so keys, tag semantics and motion settings are consistent across teams instead of re-decided per list.

## Two built-ins for two jobs Vue ships two animation wrappers in the `vue` package: - `<Transition>` animates a **single** element or component entering or leaving, typically toggled by `v-if`/`v-show` or swapped with a changing `key`. - `<TransitionGroup>` animates **each item of a list**: items being inserted, removed and, uniquely, **changing position**. ```vue <template> <TransitionGroup name="list" tag="ul"> <li v-for="item in items" :key="item.id">{{ item.text }}</li> </TransitionGroup> </template> ``` ## What stays the same `<TransitionGroup>` accepts the same props, CSS transition classes and JavaScript hook listeners as `<Transition>`: `name`, `appear`, `css`, `duration`, the custom class props, and hooks such as `@before-enter`, `@enter` and `@leave`. If you know how a named transition's classes work, you already know how each list item is animated. ## What is different | Aspect | `<Transition>` | `<TransitionGroup>` | |---|---|---| | Children | One element or component | Any number of items, usually from `v-for` | | Where classes go | The single child | **Each** item individually, never the container | | Wrapper element | None | None by default; `tag="ul"` renders one | | Position changes | Not animated | Move class applied to items that moved | | `mode` prop | `out-in` / `in-out` available | **Not supported** | | Keys | Needed only when swapping same-type elements | **Always required** on every child | ## Why keys are mandatory For a list, Vue has to answer three questions on every update: which items are new, which are gone and which are still there but in a different place. The **`key`** on each child is how Vue matches old and new children. Without it Vue cannot attach enter, leave or move animations to the right element, so `<TransitionGroup>` requires a unique key on every child and, in development, warns `<TransitionGroup> children must be keyed.` when one is missing. The key must be the item's **stable identity**, such as a database id. An array index is a key in form only: after a removal or a shuffle, indexes are reassigned, so Vue sees the wrong item leaving and no item moving. ## Why there is no `mode` A transition **mode** exists for one situation: a single slot where one element leaves while another enters, and you want them sequenced (`out-in`: old leaves first) instead of overlapping. A list has no such pairing. Several items may enter while others leave and the rest move, all independently, so there is nothing for a mode to order. Vue removes `mode` from `<TransitionGroup>`'s props entirely. ## The wrapper, then and now 1. With no `tag`, `<TransitionGroup>` renders its children as a **fragment**: the items become direct children of whatever element contains the `<TransitionGroup>`. 2. With `tag="ul"` (or any element name), Vue renders that element around the items, which is what you want when the list needs its own container for layout or semantics. 3. The container never receives transition classes; only items do. ## Components as list items Items do not have to be plain elements. A `v-for` over components works the same way, as long as each component renders a **single root element** for the classes and the move transform to land on, and each carries a unique `key`: ```vue <TransitionGroup name="list" tag="ul"> <TaskRow v-for="t in tasks" :key="t.id" :task="t" /> </TransitionGroup> ``` ## Common mistakes - Putting `<Transition>` around a `v-for`: a single-element transition cannot animate many children; use `<TransitionGroup>`. - Styling the container with the transition classes, expecting it to animate; only items get them. - Assuming a wrapper exists and targeting it in CSS, when no `tag` was given. - Passing `mode` and expecting sequencing; the prop simply does not exist here. - Keying by array index, which turns removals and reorders into in-place content changes. ## Summary Use `<Transition>` for one thing appearing or swapping, `<TransitionGroup>` for a keyed list whose items appear, disappear and move. Keys are what make per-item tracking possible, the move class is what makes reordering visible, and `mode` is absent because a list has no single outgoing and incoming pair to sequence.

  • Your Vue 2 styles targeted a `span` around a transition group; why do they stop matching after upgrading to Vue 3?
    Vue 2's `<transition-group>` always rendered a root element, a `<span>` unless `tag` said otherwise. Vue 3 renders no wrapper by default, so the items become direct children of the surrounding element and a selector on that span no longer finds anything. Add `tag="span"`, or better, a semantic tag such as `ul`.
  • What happens if one child inside a `<TransitionGroup>` has no `key`?
    In development Vue warns `<TransitionGroup> children must be keyed.` and that child gets no transition hooks, so it cannot be tracked for enter, leave or move animations. Plain text children are exempt from the warning. The fix is a unique, stable key taken from the item's identity.

saying these in an interview costs you the question

  • TransitionGroup applies the transition classes to its wrapper element
  • TransitionGroup always renders a span around the list
  • mode="out-in" works on TransitionGroup to sequence list items
  • Keys are optional in TransitionGroup; the index is used automatically
  • TransitionGroup uses completely different class names from Transition