本文 AI 產出,尚未審核

Vue 3 transition-group 教學

簡介

在單頁應用程式中,列表、表格或任何需要動態增減的元素常常會出現在畫面上。若直接插入或刪除 DOM,使用者會感受到突兀的變化,甚至會誤以為畫面卡住。Vue 3 提供的 <transition-group> 正是為了解決這類集合動畫的需求:它讓多個同類型的元素在加入、移除或順序改變時,都能以平滑的過渡效果呈現。

本篇文章將從 概念、語法、實作範例 切入,並說明常見的陷阱與最佳實踐,協助你在 Vue 3 專案中快速上手並打造更具互動性的 UI。


核心概念

1. 為什麼需要 transition-group

  • <transition> 只能針對單一元素做過渡;當有 多筆資料同時變化(例如列表的排序、批次刪除)時,必須使用 <transition-group>
  • 它會自動為 每一個子元素 加上過渡類別,讓 CSS 或 JavaScript 動畫可以分別作用於 enter / leave / move 階段。

2. 基本語法

<transition-group name="fade" tag="ul">
  <li v-for="item in items" :key="item.id">{{ item.text }}</li>
</transition-group>
  • name:過渡類別的前綴(預設會產生 fade-enter-activefade-leave-activefade-move 等)。
  • tag:外層渲染的 HTML 標籤,預設是 <span>,常會改成 <ul><div> 等容器。
  • key必須 為每個子元素提供唯一鍵值,否則 Vue 無法正確追蹤元素的進出與移動。

3. 內建過渡階段

階段 說明 產生的 CSS 類別(以 name="fade" 為例)
enter 元素被插入時 fade-enter-fromfade-enter-activefade-enter-to
leave 元素被移除時 fade-leave-fromfade-leave-activefade-leave-to
move 元素位置改變時(如排序) fade-move(只在 position 發生改變時觸發)

Tip-from-to 只在 CSS 動畫或過渡(transition)時使用,若改用 JavaScript 動畫則不需要這兩個類別。

4. 使用 CSS 實作簡易淡入淡出

/* 進入 */
.fade-enter-from { opacity: 0; }
.fade-enter-to   { opacity: 1; }
.fade-enter-active { transition: opacity 0.4s ease; }

/* 離開 */
.fade-leave-from { opacity: 1; }
.fade-leave-to   { opacity: 0; }
.fade-leave-active { transition: opacity 0.3s ease; }

/* 移動(排序) */
.fade-move { transition: transform 0.3s ease; }

只要把上述 CSS 放入組件的 <style>(或全域樣式),<transition-group> 即可自動套用。

5. 進階:使用 JavaScript Hook

如果想要在過渡過程中做更細緻的控制(例如使用 GSAPanime.js),可以改寫為 JavaScript Hook

export default {
  methods: {
    // 進入動畫
    beforeEnter(el) {
      el.style.opacity = 0;
    },
    enter(el, done) {
      // 以 GSAP 播放淡入
      gsap.to(el, { opacity: 1, duration: 0.5, onComplete: done });
    },
    // 離開動畫
    leave(el, done) {
      gsap.to(el, { opacity: 0, duration: 0.3, onComplete: done });
    }
  }
}

在模板中使用:

<transition-group
  name="custom"
  tag="ul"
  @before-enter="beforeEnter"
  @enter="enter"
  @leave="leave"
>
  <li v-for="item in items" :key="item.id">{{ item.text }}</li>
</transition-group>

⚠️ 注意:使用 JavaScript Hook 時,不要再寫對應的 CSS *-enter-active*-leave-active,否則會產生衝突。


程式碼範例

以下提供 五個實用範例,從最簡單的淡入淡出,到結合排序、卡片式布局與外部動畫函式庫的完整示範。

範例 1:基本淡入淡出列表

<template>
  <div>
    <button @click="addItem">新增項目</button>
    <button @click="removeItem">移除最後一筆</button>

    <transition-group name="fade" tag="ul" class="list">
      <li v-for="item in items" :key="item.id">{{ item.text }}</li>
    </transition-group>
  </div>
</template>

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

let id = 0
const items = ref([])

function addItem() {
  items.value.push({ id: ++id, text: `項目 ${id}` })
}
function removeItem() {
  items.value.pop()
}
</script>

<style scoped>
.list {
  padding: 0;
  margin: 1rem 0;
  list-style: none;
}
.fade-enter-from,
.fade-leave-to { opacity: 0; }
.fade-enter-active,
.fade-leave-active { transition: opacity 0.4s ease; }
</style>

重點key 必須唯一,否則 Vue 會把新舊元素視為同一個,動畫不會觸發。


範例 2:加入 move 動畫的排序列表

<template>
  <div>
    <button @click="shuffle">隨機排序</button>

    <transition-group name="slide" tag="ul" class="list">
      <li v-for="item in items" :key="item.id">{{ item.text }}</li>
    </transition-group>
  </div>
</template>

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

const items = ref([
  { id: 1, text: '蘋果' },
  { id: 2, text: '香蕉' },
  { id: 3, text: '櫻桃' },
  { id: 4, text: '葡萄' }
])

function shuffle() {
  items.value = items.value
    .map(v => ({ v, r: Math.random() }))
    .sort((a, b) => a.r - b.r)
    .map(({ v }) => v)
}
</script>

<style scoped>
.list { list-style: none; padding: 0; }
.slide-enter-from,
.slide-leave-to { opacity: 0; transform: translateY(-10px); }
.slide-enter-active,
.slide-leave-active { transition: all 0.3s ease; }
.slide-move { transition: transform 0.4s ease; }
</style>

說明:當 shuffle 重新排列陣列時,<transition-group> 會偵測到 位置變動,自動套用 slide-move,產生滑動效果。


範例 3:卡片式布局 + GSAP 動畫

<template>
  <div class="grid">
    <button @click="addCard">新增卡片</button>

    <transition-group name="card" tag="div" class="cards"
      @before-enter="beforeEnter"
      @enter="enter"
      @leave="leave"
    >
      <div class="card" v-for="c in cards" :key="c.id">
        {{ c.title }}
      </div>
    </transition-group>
  </div>
</template>

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

let id = 0
const cards = ref([])

function addCard() {
  cards.value.push({ id: ++id, title: `卡片 ${id}` })
}

/* ---------- GSAP Hook ---------- */
function beforeEnter(el) {
  el.style.opacity = 0
  el.style.transform = 'scale(0.8)'
}
function enter(el, done) {
  gsap.to(el, {
    opacity: 1,
    scale: 1,
    duration: 0.5,
    ease: 'back.out(1.7)',
    onComplete: done
  })
}
function leave(el, done) {
  gsap.to(el, {
    opacity: 0,
    scale: 0.5,
    duration: 0.3,
    ease: 'power1.in',
    onComplete: done
  })
}
</script>

<style scoped>
.grid { padding: 1rem; }
.cards {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}
.card {
  width: 120px;
  height: 80px;
  background: #4caf50;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
}
</style>

關鍵:使用 @before-enter@enter@leave 讓每張卡片在加入/移除時都有 彈性縮放 的效果,提升 UI 的動態感。


範例 4:使用 appear 讓首次渲染也有過渡

<template>
  <transition-group name="fade" appear tag="ul" class="list">
    <li v-for="item in items" :key="item.id">{{ item.text }}</li>
  </transition-group>
</template>

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

const items = ref([])

onMounted(() => {
  // 模擬從 API 取得資料
  setTimeout(() => {
    items.value = [
      { id: 1, text: 'Vue' },
      { id: 2, text: 'React' },
      { id: 3, text: 'Angular' }
    ]
  }, 500)
})
</script>

<style scoped>
.fade-enter-from,
.fade-leave-to { opacity: 0; }
.fade-enter-active,
.fade-leave-active { transition: opacity 0.6s ease; }
/* appear 用的同樣類別 */
</style>

說明:加上 appear 後,即使是首次渲染(mounted 時)也會套用 enter 動畫,讓頁面載入更具層次感。


範例 5:結合 v-move 自訂搬移動畫(不使用 CSS move

<template>
  <transition-group tag="ul" class="list"
    @move="onMove"
  >
    <li v-for="item in items" :key="item.id">{{ item.text }}</li>
  </transition-group>
</template>

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

const items = ref([
  { id: 1, text: 'A' },
  { id: 2, text: 'B' },
  { id: 3, text: 'C' }
])

function onMove({ relatedTarget, el }) {
  // 這裡使用原生 Web Animations API
  el.animate(
    [{ transform: 'translateY(20px)' }, { transform: 'translateY(0)' }],
    { duration: 300, easing: 'ease-out' }
  )
}
</script>

<style scoped>
.list { list-style: none; padding: 0; }
</style>

技巧:當你想要 自訂搬移動畫(例如不同方向、不同時間),可以直接監聽 @move,並使用 Web Animations APIGSAP 等自行控制。


常見陷阱與最佳實踐

陷阱 原因 解決方式
忘記設定 key Vue 無法辨識元素是「新增」還是「更新」 為每筆資料提供唯一且 不變key(如資料庫主鍵)
過渡類別衝突 同時使用 CSS *-enter-active 與 JavaScript Hook 只保留一種方式;若使用 Hook,移除對應的 CSS 類別
move 動畫不起作用 元素的 position 沒有改變,或使用了 display: inline 確保元素是 可搬移的display: blockflexgrid),或自行在 @move 中實作
過渡時間過長/過短 使用者感受不佳,或動畫與資料更新不同步 調整 transition / animation 時間,並在 JavaScript Hook 中呼叫 done 於正確時機
大量資料同時加入 每個元素都要觸發過渡,可能造成瀏覽器卡頓 使用 v-show分批 加入(例如 requestAnimationFrame)或降低動畫複雜度

最佳實踐

  1. 始終使用 key,且盡量使用 不可變 的值(例如 uuid)。
  2. 針對 簡單淡入淡出 直接使用 CSS,僅在需要複雜控制時才切換到 JavaScript Hook。
  3. 若列表項目數量 超過 30 筆,考慮 虛擬滾動(如 vue-virtual-scroller)搭配 transition-group,減少 DOM 變更量。
  4. 在大型專案中,將過渡樣式抽成 SCSS 變數或 mixin,保持風格一致。
  5. 測試在不同瀏覽器的 性能表現,特別是手機端,確保過渡不會導致卡頓。

實際應用場景

場景 為什麼適合使用 transition-group
聊天訊息列表 新訊息插入、舊訊息刪除、載入更多時需要自然的滑入滑出效果
待辦清單 勾選完成後的淡出、重新排序(拖曳)時的搬移動畫提升使用者體驗
商品卡片牆 篩選或分頁切換時,卡片的進出與位置變動可使用 transition-group 讓頁面更流暢
儀表板圖表切換 多個圖表元件同時顯示/隱藏,透過 transition-group 渲染切換動畫,避免突兀感
彈性表格(DataTable) 行的新增、刪除與排序(點擊欄位排序)皆可使用 transition-group 產生視覺上的提示

案例示範:在一個待辦清單 App 中,使用者拖曳改變項目順序時,只要在 v-for 裡加上 <transition-group>,Vue 會自動算出每筆資料的搬移距離,套用 move 動畫,讓拖曳感受更自然。


總結

  • <transition-group> 是 Vue 3 處理 集合動畫 的核心工具,能讓列表、卡片或任何多元素的變化呈現流暢過渡。
  • 只要正確設定 keynametag,並配合 CSSJavaScript Hook,即可實作淡入淡出、滑動、彈性縮放等多樣效果。
  • 常見的陷阱大多與 鍵值遺失、類別衝突搬移條件不符 有關,遵循最佳實踐能有效避免。
  • 在實務開發中,transition-group 常被用於聊天、待辦清單、商品卡片牆等需要即時更新 UI 的場景,提升使用者的互動感受與產品的專業度。

掌握了 transition-group,你就能在 Vue 3 專案裡為任何動態集合增添自然、好看的過渡效果,讓前端介面更具生命力。祝開發順利! 🎉