Vue3 – 樣式與 CSS 管理
主題:CSS Transition 與 Animation
簡介
在單頁應用程式中,使用者介面的流暢度直接影響使用者體驗。CSS Transition 與 CSS Animation 是前端開發者最常用的兩種動畫手段,配合 Vue3 的 <transition>、<transition-group> 元件,能以最小的程式碼量為元素增添自然的過渡與動態效果。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你掌握在 Vue3 中使用 CSS 動畫的技巧,讓你的專案不只功能完整,也能呈現「細膩」的 UI 交互。
核心概念
1. CSS Transition 基本原理
transition 讓屬性的變化在 起始值 與 結束值 之間以指定的時間、緩動函式(easing)平滑過渡。最簡單的寫法如下:
/* 讓寬度在 0.3 秒內平滑變化 */
.box {
transition: width 0.3s ease-out;
}
在 Vue 中,只要在元素的 class 切換或 v-show、v-if 產生的顯隱變化,符合 CSS 設定的屬性,就會自動觸發 transition。
2. Vue3 的 <transition> 元件
Vue 為每個過渡階段自動套用特定的 class 名稱,預設規則如下(以 fade 為例):
| 階段 | class name |
|---|---|
| 進入前 (enter-from) | fade-enter-from |
| 進入中 (enter-active) | fade-enter-active |
| 進入後 (enter-to) | fade-enter-to |
| 離開前 (leave-from) | fade-leave-from |
| 離開中 (leave-active) | fade-leave-active |
| 離開後 (leave-to) | fade-leave-to |
只要在 CSS 中為這些 class 定義 transition 或 animation,Vue 便會在元素掛載/卸載時自動播放。
範例 1:淡入淡出
<template>
<button @click="show = !show">Toggle</button>
<!-- 使用預設的 fade 過渡 -->
<transition name="fade">
<div v-if="show" class="box">Hello Vue3</div>
</transition>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(false)
</script>
<style scoped>
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.fade-enter-to,
.fade-leave-from {
opacity: 1;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.4s ease;
}
.box {
padding: 12px;
background: #42b983;
color: #fff;
border-radius: 4px;
}
</style>
重點:
name="fade"讓 Vue 自動產生fade-xxxx系列 class,開發者只需寫對應的樣式即可。
3. CSS Animation 與 @keyframes
animation 允許在多個關鍵幀(keyframe)之間自動播放,且可以設定迭代次數、方向、延遲等。與 transition 最大的差別是 不需要屬性變化的觸發點,只要把 animation 加到元素上,動畫就會自動執行。
範例 2:旋轉圖示
<template>
<button @click="spinning = !spinning">
<i :class="['icon', { spin: spinning }]">🔄</i>
切換旋轉
</button>
</template>
<script setup>
import { ref } from 'vue'
const spinning = ref(false)
</script>
<style scoped>
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spin {
animation: rotate 1.2s linear infinite;
}
.icon {
display: inline-block;
font-size: 1.5rem;
}
</style>
在這個例子中,我們僅透過 class 切換 (spin) 來啟動或停止動畫,不需要 Vue 介入任何生命週期。
4. <transition-group>:列表動畫
當要對 多筆資料(如 v-for)做新增、刪除或排序動畫時,<transition-group> 能同時為每個項目套用過渡效果。它會自動為每個子元素產生 v-move、v-enter、v-leave 等 class。
範例 3:彈性列表
<template>
<button @click="add">新增項目</button>
<button @click="shuffle">隨機排序</button>
<transition-group name="list" tag="ul" class="list">
<li v-for="item in items" :key="item.id" class="list-item">
{{ item.text }}
<button @click="remove(item.id)">✕</button>
</li>
</transition-group>
</template>
<script setup>
import { ref } from 'vue'
let id = 0
const items = ref([])
function add() {
items.value.push({ id: ++id, text: `項目 ${id}` })
}
function remove(id) {
items.value = items.value.filter(i => i.id !== id)
}
function shuffle() {
items.value = items.value
.map(v => ({ v, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ v }) => v)
}
</script>
<style scoped>
.list {
padding: 0;
margin: 0;
}
.list-item {
list-style: none;
margin: 8px 0;
padding: 8px 12px;
background: #f5f5f5;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
/* 進入/離開動畫 */
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(-20px);
}
.list-enter-active,
.list-leave-active {
transition: all 0.35s ease;
}
/* 移動動畫 (排序) */
.list-move {
transition: transform 0.4s ease;
}
</style>
技巧:
<transition-group>必須指定tag(如ul)與key,否則 Vue 無法辨識哪一筆是「同一個」元素,動畫將失效。
5. 動態過渡名稱與 CSS 變數
在大型專案中,往往會有多種過渡樣式。透過 動態 name 或 CSS 變數,可以讓同一套 Vue 組件支援不同的動畫需求。
範例 4:切換不同過渡
<template>
<select v-model="mode">
<option value="fade">淡入淡出</option>
<option value="slide">滑動</option>
</select>
<transition :name="mode">
<div v-if="show" class="box">{{ mode }} 中</div>
</transition>
<button @click="show = !show">Toggle</button>
</template>
<script setup>
import { ref } from 'vue'
const mode = ref('fade')
const show = ref(true)
</script>
<style scoped>
/* fade */
.fade-enter-from,
.fade-leave-to { opacity: 0; }
.fade-enter-active,
.fade-leave-active { transition: opacity 0.3s ease; }
/* slide */
.slide-enter-from { transform: translateY(-20px); opacity: 0; }
.slide-enter-to { transform: translateY(0); opacity: 1; }
.slide-leave-from { transform: translateY(0); opacity: 1; }
.slide-leave-to { transform: translateY(20px); opacity: 0; }
.slide-enter-active,
.slide-leave-active { transition: all 0.35s ease; }
.box {
margin-top: 12px;
padding: 10px;
background: #ff9f43;
color: #fff;
border-radius: 4px;
}
</style>
重點:
:name="mode"讓過渡類型可由使用者或程式動態決定,提升元件的可重用性。
6. 結合 JavaScript Hook 控制動畫
Vue 仍提供 JavaScript 鉤子(beforeEnter、enter、afterEnter…),當 CSS 無法滿足特殊需求時,可在這些鉤子裡使用 requestAnimationFrame、setTimeout 等手動控制動畫。
範例 5:自訂高度過渡(從 0 到 auto)
<template>
<button @click="open = !open">Toggle 高度</button>
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
>
<div v-show="open" class="content">
這是一段會自動伸縮的文字,內容可以很長很長...
</div>
</transition>
</template>
<script setup>
import { ref } from 'vue'
const open = ref(false)
function beforeEnter(el) {
el.style.height = '0'
}
function enter(el, done) {
const height = el.scrollHeight
el.style.transition = 'height 0.35s ease'
el.style.height = height + 'px'
el.addEventListener('transitionend', done, { once: true })
}
function afterEnter(el) {
el.style.height = 'auto'
}
function beforeLeave(el) {
el.style.height = el.scrollHeight + 'px'
}
function leave(el, done) {
// 觸發 reflow
void el.offsetWidth
el.style.transition = 'height 0.35s ease'
el.style.height = '0'
el.addEventListener('transitionend', done, { once: true })
}
function afterLeave(el) {
el.style.height = ''
}
</script>
<style scoped>
.content {
overflow: hidden;
background: #e0f7fa;
padding: 12px;
margin-top: 8px;
border-radius: 4px;
}
</style>
此範例展示了 「從 0 到 auto」 的過渡,純 CSS 難以實現,藉由 JavaScript 鉤子即可完成。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 最佳做法 |
|---|---|---|
忘記 key |
在 <transition-group> 中若未提供唯一的 key,Vue 會把所有項目視為同一個,導致動畫失效或錯位。 |
為每個 v-for 項目設定穩定且唯一的 :key(如資料庫 ID)。 |
過度使用 !important |
transition/animation 受 CSS 優先權影響,!important 可能讓預期的過渡被覆寫。 |
盡量透過 scoped 或 CSS Modules 管理樣式,避免使用 !important。 |
同時使用 v-show 與 v-if |
v-show 僅切換 display,不會觸發 enter/leave;v-if 會重新掛載元素,兩者混用容易產生不一致的動畫。 |
根據需求選擇其一:需要保留狀態用 v-show,需要重新渲染用 v-if。 |
| 動畫時間與實際變化不符 | transition 的時間若寫得過長或過短,使用者會感到卡頓或突兀。 |
依照屬性變化的「感官」長度調整 duration,常見 200‑400ms 為最佳體驗。 |
未設定 transform 的硬體加速 |
大量動畫可能造成卡頓,尤其在手機上。 | 使用 transform: translate3d(0,0,0) 或 will-change: transform, opacity 讓瀏覽器啟用 GPU 加速。 |
| 過度堆疊多層動畫 | 同時套用 transition、animation、JavaScript 鉤子,會產生衝突。 |
優先選擇一種方式,若必須混合,確保時間軸不重疊,且在 CSS 中使用 animation-fill-mode 或 transition-delay 進行協調。 |
建議的開發流程
- 先寫 CSS:先在純 HTML/CSS 中驗證過渡或動畫是否正常。
- 加入 Vue
<transition>:確認 class 名稱對應正確,並使用scoped防止全域污染。 - 測試不同裝置:使用 Chrome DevTools 的「Device Mode」或實機測試,觀察是否有掉幀。
- 若需高度自動:使用 JavaScript 鉤子或
@keyframes搭配max-height,避免height: auto無法被 transition 的限制。 - 最後加入可自訂的 props:將過渡名稱、時間、緩動函式抽成元件的
props,提升可重用性。
實際應用場景
| 場景 | 為何需要動畫 | 建議使用方式 |
|---|---|---|
| 表單驗證錯誤提示 | 錯誤訊息出現時若瞬間顯示,使用者容易忽略。 | 使用 <transition name="slide-down">,讓錯誤訊息從上方滑入並淡出。 |
| 彈出式對話框 (Modal) | 背景遮罩與對話框同時淡入,提升聚焦感。 | <transition name="fade"> 包裹遮罩,<transition name="scale"> 包裹對話框,兩者同時觸發。 |
| 清單排序/拖放 | 列表項目位置變動時若沒有動畫,會讓使用者感到突兀。 | <transition-group name="list"> 搭配 list-move,自動產生平滑移動效果。 |
| 圖片輪播 | 圖片切換時需要淡入淡出或滑動過渡,提升視覺連貫性。 | 使用 transition 的 enter-active/leave-active,或 animation 的 keyframes 實作無縫切換。 |
| 側邊欄展開/收合 | 大量內容在側邊欄展開時需要同時調整寬度與透明度。 | 結合 width 與 opacity 的 transition,或使用 transform: translateX 搭配 will-change 提升效能。 |
| Loading Skeleton | 資料載入期間的骨架屏需要閃爍或脈衝效果。 | 使用 @keyframes pulse 的 animation,配合 v-if 控制顯示與隱藏。 |
總結
- CSS Transition 讓屬性變化在固定時間內平滑過渡,適合單一屬性的簡單動畫。
- CSS Animation 透過
@keyframes可製作更複雜、持續或迭代的動畫,且不依賴狀態變化觸發。 - Vue3 的
<transition>與<transition-group>為動畫提供了 自動 class 注入、生命週期鉤子 與 動態名稱 等便利功能,讓開發者只需關注樣式本身。 - 常見陷阱包括忘記
key、混用v-show/v-if、以及忽略 GPU 加速。遵循「先寫 CSS、再加入 Vue」的流程,可減少除錯成本。 - 在實務上,從表單驗證、彈窗、列表排序到 Loading Skeleton,皆能透過上述技巧提升使用者體驗與介面品質。
掌握了 CSS transition 與 animation 的核心概念與 Vue3 的整合方式,你就能在專案中快速加入流暢、易維護的動畫效果,讓你的前端作品更具「活力」與「專業感**。」