<TransitionGroup> animates multiple elements in a list, including additions, removals, and reordering.

Key Differences from Transition

TransitionTransitionGroup
Single childMultiple children
No wrapper elementRenders wrapper (default: <span>)
v-if/v-showv-for with :key

Basic Usage

vue
<script setup>
import { ref } from 'vue'

const items = ref([1, 2, 3])
let id = 4

function add() {
  items.value.push(id++)
}

function remove(item: number) {
  items.value = items.value.filter(i => i !== item)
}
</script>

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

<style>
.list-enter-active,
.list-leave-active {
  transition: all 0.3s ease;
}

.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(-30px);
}
</style>

The tag Prop

vue
<!-- Renders as <ul> -->
<TransitionGroup tag="ul">
  <li v-for="item in items" :key="item">{{ item }}</li>
</TransitionGroup>

<!-- Renders as <div> -->
<TransitionGroup tag="div" class="grid">
  <div v-for="item in items" :key="item">{{ item }}</div>
</TransitionGroup>

<!-- No wrapper (Vue 3.5+) -->
<TransitionGroup :tag="null">
  <div v-for="item in items" :key="item">{{ item }}</div>
</TransitionGroup>

Move Transitions

Animate items moving to new positions:

vue
<TransitionGroup name="list" tag="ul">
  <li v-for="item in items" :key="item">{{ item }}</li>
</TransitionGroup>

<style>
/* Enter and leave animations */
.list-enter-active,
.list-leave-active {
  transition: all 0.5s ease;
}

.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

/* IMPORTANT: Move animation */
.list-move {
  transition: transform 0.5s ease;
}

/* Fix leaving items taking space */
.list-leave-active {
  position: absolute;
}
</style>

Staggered Animations

Delay animations based on index:

vue
<script setup>
function onBeforeEnter(el: HTMLElement) {
  el.style.opacity = '0'
  el.style.transform = 'translateY(20px)'
}

function onEnter(el: HTMLElement, done: () => void) {
  const delay = parseInt(el.dataset.index || '0') * 100
  
  setTimeout(() => {
    el.style.transition = 'all 0.3s ease'
    el.style.opacity = '1'
    el.style.transform = 'translateY(0)'
    
    setTimeout(done, 300)
  }, delay)
}
</script>

<template>
  <TransitionGroup 
    tag="ul"
    @before-enter="onBeforeEnter"
    @enter="onEnter"
  >
    <li 
      v-for="(item, index) in items" 
      :key="item.id"
      :data-index="index"
    >
      {{ item.name }}
    </li>
  </TransitionGroup>
</template>

Shuffle Animation

vue
<script setup>
import { ref } from 'vue'

const items = ref([1, 2, 3, 4, 5, 6, 7, 8, 9])

function shuffle() {
  items.value = [...items.value].sort(() => Math.random() - 0.5)
}
</script>

<template>
  <button @click="shuffle">Shuffle</button>
  
  <TransitionGroup name="shuffle" tag="div" class="grid">
    <div v-for="item in items" :key="item" class="item">
      {{ item }}
    </div>
  </TransitionGroup>
</template>

<style>
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}

.item {
  padding: 20px;
  background: #42b883;
  color: white;
  text-align: center;
}

.shuffle-move {
  transition: transform 0.5s ease;
}
</style>

Filter/Sort Animation

vue
<script setup>
import { ref, computed } from 'vue'

const items = ref([
  { id: 1, name: 'Apple', category: 'fruit' },
  { id: 2, name: 'Carrot', category: 'vegetable' },
  { id: 3, name: 'Banana', category: 'fruit' },
  { id: 4, name: 'Broccoli', category: 'vegetable' },
])

const filter = ref('all')

const filteredItems = computed(() => {
  if (filter.value === 'all') return items.value
  return items.value.filter(i => i.category === filter.value)
})
</script>

<template>
  <select v-model="filter">
    <option value="all">All</option>
    <option value="fruit">Fruits</option>
    <option value="vegetable">Vegetables</option>
  </select>
  
  <TransitionGroup name="filter" tag="ul">
    <li v-for="item in filteredItems" :key="item.id">
      {{ item.name }}
    </li>
  </TransitionGroup>
</template>

<style>
.filter-enter-active,
.filter-leave-active {
  transition: all 0.3s ease;
}

.filter-enter-from,
.filter-leave-to {
  opacity: 0;
  transform: scale(0.9);
}

.filter-move {
  transition: transform 0.3s ease;
}

.filter-leave-active {
  position: absolute;
}
</style>
✓ Completed