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-active、fade-leave-active、fade-move等)。tag:外層渲染的 HTML 標籤,預設是<span>,常會改成<ul>、<div>等容器。key:必須 為每個子元素提供唯一鍵值,否則 Vue 無法正確追蹤元素的進出與移動。
3. 內建過渡階段
| 階段 | 說明 | 產生的 CSS 類別(以 name="fade" 為例) |
|---|---|---|
| enter | 元素被插入時 | fade-enter-from → fade-enter-active → fade-enter-to |
| leave | 元素被移除時 | fade-leave-from → fade-leave-active → fade-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
如果想要在過渡過程中做更細緻的控制(例如使用 GSAP、anime.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 API、GSAP等自行控制。
常見陷阱與最佳實踐
| 陷阱 | 原因 | 解決方式 |
|---|---|---|
忘記設定 key |
Vue 無法辨識元素是「新增」還是「更新」 | 為每筆資料提供唯一且 不變 的 key(如資料庫主鍵) |
| 過渡類別衝突 | 同時使用 CSS *-enter-active 與 JavaScript Hook |
只保留一種方式;若使用 Hook,移除對應的 CSS 類別 |
move 動畫不起作用 |
元素的 position 沒有改變,或使用了 display: inline |
確保元素是 可搬移的(display: block、flex、grid),或自行在 @move 中實作 |
| 過渡時間過長/過短 | 使用者感受不佳,或動畫與資料更新不同步 | 調整 transition / animation 時間,並在 JavaScript Hook 中呼叫 done 於正確時機 |
| 大量資料同時加入 | 每個元素都要觸發過渡,可能造成瀏覽器卡頓 | 使用 v-show 或 分批 加入(例如 requestAnimationFrame)或降低動畫複雜度 |
最佳實踐
- 始終使用
key,且盡量使用 不可變 的值(例如uuid)。 - 針對 簡單淡入淡出 直接使用 CSS,僅在需要複雜控制時才切換到 JavaScript Hook。
- 若列表項目數量 超過 30 筆,考慮 虛擬滾動(如
vue-virtual-scroller)搭配transition-group,減少 DOM 變更量。 - 在大型專案中,將過渡樣式抽成 SCSS 變數或 mixin,保持風格一致。
- 測試在不同瀏覽器的 性能表現,特別是手機端,確保過渡不會導致卡頓。
實際應用場景
| 場景 | 為什麼適合使用 transition-group |
|---|---|
| 聊天訊息列表 | 新訊息插入、舊訊息刪除、載入更多時需要自然的滑入滑出效果 |
| 待辦清單 | 勾選完成後的淡出、重新排序(拖曳)時的搬移動畫提升使用者體驗 |
| 商品卡片牆 | 篩選或分頁切換時,卡片的進出與位置變動可使用 transition-group 讓頁面更流暢 |
| 儀表板圖表切換 | 多個圖表元件同時顯示/隱藏,透過 transition-group 渲染切換動畫,避免突兀感 |
| 彈性表格(DataTable) | 行的新增、刪除與排序(點擊欄位排序)皆可使用 transition-group 產生視覺上的提示 |
案例示範:在一個待辦清單 App 中,使用者拖曳改變項目順序時,只要在
v-for裡加上<transition-group>,Vue 會自動算出每筆資料的搬移距離,套用move動畫,讓拖曳感受更自然。
總結
<transition-group>是 Vue 3 處理 集合動畫 的核心工具,能讓列表、卡片或任何多元素的變化呈現流暢過渡。- 只要正確設定
key、name、tag,並配合 CSS 或 JavaScript Hook,即可實作淡入淡出、滑動、彈性縮放等多樣效果。 - 常見的陷阱大多與 鍵值遺失、類別衝突 或 搬移條件不符 有關,遵循最佳實踐能有效避免。
- 在實務開發中,
transition-group常被用於聊天、待辦清單、商品卡片牆等需要即時更新 UI 的場景,提升使用者的互動感受與產品的專業度。
掌握了 transition-group,你就能在 Vue 3 專案裡為任何動態集合增添自然、好看的過渡效果,讓前端介面更具生命力。祝開發順利! 🎉