Vue.js: 过渡与动画
最后更新:2026-08-26
过渡与动画让你的 Vue 应用更生动——元素进入/离开时平滑过渡,列表变化时优雅重排。Vue 内置了 <Transition> 和 <TransitionGroup> 组件,配合 CSS 类名或 JS 钩子实现各种动画效果。
掌握过渡动画是提升用户体验的关键——好的动画让应用感觉"流畅、响应式、专业"。
1. 你将学到
<Transition>单元素进入/离开动画- 6 种 CSS class 钩子(v-enter-from / v-enter-active 等)
<TransitionGroup>列表动画- JavaScript 钩子(@before-enter / @enter 等)
- 第三方动画库集成(@vueuse/motion、GSAP)
- Animate.css 集成
- 5 大实战场景
2. 一个"切换 Tab"硬切硬换的尴尬
(1) 痛点:组件突然消失/出现,UI 闪烁
Alice 的后台有个 Tab 切换器:
VUE
<!-- ❌ 翻车版:v-if 突然切换 -->
<template>
<button @click="currentTab = 'home'">Home</button>
<button @click="currentTab = 'profile'">Profile</button>
<div v-if="currentTab === 'home'">Home Content</div>
<div v-else-if="currentTab === 'profile'">Profile Content</div>
</template>
用户体验
- Tab 切换时,突然出现/消失
- 没有过渡动画,感觉"生硬"
- 专业感不足
(2) Vue Transition 解法:1 个标签,6 个 class
VUE
<!-- ✅ 正确版:平滑过渡 -->
<template>
<button @click="currentTab = 'home'">Home</button>
<button @click="currentTab = 'profile'">Profile</button>
<Transition name="fade" mode="out-in">
<div v-if="currentTab === 'home'" key="home">Home Content</div>
<div v-else-if="currentTab === 'profile'" key="profile">Profile Content</div>
</Transition>
</template>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
</style>
用户体验
- Tab 切换时,平滑淡入淡出
- 0.3 秒过渡,专业感强
- 6 个 CSS class 自动应用
(3) 收益
加上过渡后:
- 用户体验:从"生硬"到"流畅"
- 专业感:显著提升
- 代码量:1 个
<Transition>标签 + 6 个 CSS class - 可定制:4 种 transition name + 3 种 mode
3. Transition 单元素
(1) 基本用法
VUE
<template>
<button @click="show = !show">Toggle</button>
<Transition name="fade">
<p v-if="show">Hello Vue</p>
</Transition>
</template>
<style>
/* 6 个 CSS class,Vue 自动添加/移除 */
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
</style>
(2) 6 种 CSS class 详解
| Class | 触发时机 | 用途 |
|---|---|---|
v-enter-from |
元素插入前 | 起始状态 |
v-enter-active |
元素插入整个过程 | 过渡过程 |
v-enter-to |
元素插入后 | 结束状态 |
v-leave-from |
元素离开前 | 起始状态 |
v-leave-active |
元素离开整个过程 | 过渡过程 |
v-leave-to |
元素离开后 | 结束状态 |
(3) 命名规则
VUE
<!-- v-bind:name="fade" → 6 个 class 是 .fade-xxx -->
<Transition name="fade">...</Transition>
<!-- 不写 name → 默认 v-xxx(6 个 class 是 .v-xxx)-->
<Transition>...</Transition>
<!-- 动态 name -->
<Transition :name="transitionName">...</Transition>
(4) 3 种模式
VUE
<!-- 1. 默认模式:进入和离开同时进行(重叠) -->
<Transition name="fade">
<div v-if="show">Content</div>
</Transition>
<!-- 2. out-in:先离开后进入(推荐,避免重叠) -->
<Transition name="fade" mode="out-in">
<div v-if="show">Content</div>
</Transition>
<!-- 3. in-out:先进入后离开(少见) -->
<Transition name="fade" mode="in-out">
<div v-if="show">Content</div>
</Transition>
4. 5 种 CSS 动画
(1) 淡入淡出(最常用)
CSS
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
(2) 滑动
CSS
.slide-enter-active, .slide-leave-active {
transition: transform 0.3s;
}
.slide-enter-from {
transform: translateX(-100%);
}
.slide-leave-to {
transform: translateX(100%);
}
(3) 缩放
CSS
.scale-enter-active, .scale-leave-active {
transition: transform 0.3s;
}
.scale-enter-from, .scale-leave-to {
transform: scale(0);
}
(4) 旋转
CSS
.rotate-enter-active {
transition: transform 1s;
}
.rotate-enter-from {
transform: rotate(0deg);
}
.rotate-leave-active {
animation: bounce 0.5s;
}
@keyframes bounce {
0% { transform: scale(0); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
(5) 组合动画
CSS
.combo-enter-active, .combo-leave-active {
transition: all 0.3s;
}
.combo-enter-from {
opacity: 0;
transform: translateX(-50px) scale(0.8);
}
.combo-leave-to {
opacity: 0;
transform: translateX(50px) scale(0.8);
}
5. TransitionGroup 列表动画
(1) 基本用法
VUE
<template>
<button @click="add">Add</button>
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.name }}
<button @click="remove(item.id)">×</button>
</li>
</TransitionGroup>
</template>
<script setup>
const { ref } = Vue
const items = ref([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' }
])
function add() {
const id = Date.now()
items.value.push({ id, name: `Item ${id}` })
}
function remove(id) {
items.value = items.value.filter(i => i.id !== id)
}
</script>
<style>
/* 6 个 class,但用于列表项的 enter/leave/move */
.list-enter-active, .list-leave-active {
transition: all 0.5s;
}
.list-enter-from, .list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-leave-active {
position: absolute; /* 离开时脱离文档流 */
}
/* 列表项移动时的动画 */
.list-move {
transition: transform 0.5s;
}
</style>
(2) 关键点:.list-move
CSS
/* 列表项位置变化时(如排序),自动应用 .list-move */
.list-move {
transition: transform 0.5s;
}
(3) 5 大列表动画场景
| 场景 | 动画 |
|---|---|
| 添加项 | 淡入 + 下滑 |
| 删除项 | 淡出 + 左滑 |
| 排序 | 平滑移动到新位置 |
| 筛选 | 离开的淡出,保留的平滑过渡 |
| 拖拽 | 实时跟随鼠标 |
6. JavaScript 钩子
(1) 8 个 JS 钩子
VUE
<template>
<Transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
:css="false" <!-- 禁用 CSS class -->
>
<p v-if="show">Hello</p>
</Transition>
</template>
<script setup>
import gsap from 'gsap'
function beforeEnter(el) {
console.log('Before enter:', el)
}
function enter(el, done) {
gsap.to(el, {
opacity: 1,
y: 0,
duration: 0.5,
onComplete: done // 通知完成
})
}
function afterEnter(el) {
console.log('After enter:', el)
}
// leave / beforeLeave / afterLeave 类似
</script>
(2) GSAP 集成(推荐)
BASH
npm install gsap
VUE
<template>
<Transition :css="false" @enter="onEnter" @leave="onLeave">
<p v-if="show">Hello GSAP</p>
</Transition>
</template>
<script setup>
import gsap from 'gsap'
function onEnter(el, done) {
gsap.fromTo(el,
{ opacity: 0, y: 50 },
{ opacity: 1, y: 0, duration: 0.5, onComplete: done }
)
}
function onLeave(el, done) {
gsap.to(el, { opacity: 0, y: -50, duration: 0.3, onComplete: done })
}
</script>
7. 第三方动画库
(1) Animate.css 集成
BASH
npm install animate.css
JS
// main.js
import 'animate.css'
VUE
<template>
<Transition
enter-active-class="animate__animated animate__fadeIn"
leave-active-class="animate__animated animate__fadeOut"
>
<p v-if="show">Hello</p>
</Transition>
</template>
(2) @vueuse/motion
BASH
npm install @vueuse/motion
JS
// main.js
import { MotionPlugin } from '@vueuse/motion'
app.use(MotionPlugin)
VUE
<template>
<div v-motion :initial="{ opacity: 0, y: 50 }" :enter="{ opacity: 1, y: 0 }">
Fade in from below
</div>
</template>
(3) GSAP 推荐场景
| 库 | 推荐度 | 适用 |
|---|---|---|
| CSS 动画 | ⭐⭐⭐⭐⭐ | 简单过渡、淡入淡出 |
| Animate.css | ⭐⭐⭐⭐ | 大量预设动画 |
| @vueuse/motion | ⭐⭐⭐⭐ | 声明式、Vue 集成好 |
| GSAP | ⭐⭐⭐⭐⭐ | 复杂时间线、物理动画 |
8. 完整示例:5 大过渡场景
▶ 示例:5 种 Vue Transition 动画(CDN 全支持)
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
button { padding: 6px 12px; margin: 4px; background: #42b883; color: white; border: none; border-radius: 4px; cursor: pointer; }
.modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; }
.modal { background: white; padding: 2rem; border-radius: 8px; min-width: 300px; }
/* 5 种动画 class */
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
.slide-enter-active, .slide-leave-active { transition: transform 0.3s; }
.slide-enter-from { transform: translateX(-100%); }
.slide-leave-to { transform: translateX(100%); }
.scale-enter-active, .scale-leave-active { transition: transform 0.3s; }
.scale-enter-from, .scale-leave-to { transform: scale(0); }
.combo-enter-active, .combo-leave-active { transition: all 0.3s; }
.combo-enter-from { opacity: 0; transform: translateY(-30px); }
.list-enter-active, .list-leave-active { transition: all 0.5s; }
.list-leave-to { opacity: 0; transform: translateX(-30px); }
.list-move { transition: transform 0.5s; }
</style>
<div id="app">
<h4>1. Fade 淡入淡出</h4>
<button @click="showFade = !showFade">{{ showFade ? '隐藏' : '显示' }}</button>
<transition name="fade">
<p v-if="showFade" style="padding: 0.5rem; background: #f0f9ff;">👋 Hello Fade</p>
</transition>
<h4>2. Slide 滑动</h4>
<transition name="slide">
<p v-if="showSlide" key="slide-text" style="padding: 0.5rem; background: #fef3c7;">👉 Slide 滑入</p>
</transition>
<button @click="showSlide = !showSlide">切换</button>
<h4>3. Scale 缩放(Modal 弹窗)</h4>
<button @click="showModal = true">打开 Modal</button>
<transition name="scale">
<div v-if="showModal" class="modal-bg" @click.self="showModal = false">
<div class="modal">
<h3>弹窗内容</h3>
<button @click="showModal = false">关闭</button>
</div>
</div>
</transition>
<h4>4. TransitionGroup 列表动画</h4>
<input v-model="newItem" placeholder="新项目">
<button @click="addItem">添加</button>
<transition-group name="list" tag="ul" style="list-style: none; padding: 0;">
<li v-for="item in items" :key="item.id" style="padding: 4px; background: #ecfdf5; margin: 2px 0;">
{{ item.name }} <button @click="removeItem(item.id)">×</button>
</li>
</transition-group>
<h4>5. Combo 组合(Tab 切换)</h4>
<button v-for="tab in tabs" :key="tab" @click="currentTab = tab">{{ tab }}</button>
<transition name="combo" mode="out-in">
<p :key="currentTab" style="padding: 0.5rem; background: #eff6ff;">
当前 Tab: {{ currentTab }}
</p>
</transition>
</div>
<script>
const { createApp, ref } = Vue
const App = {
setup() {
const showFade = ref(true)
const showSlide = ref(true)
const showModal = ref(false)
const currentTab = ref('Home')
const tabs = ['Home', 'Profile', 'Settings']
const newItem = ref('')
const items = ref([
{ id: 1, name: '项目 1' },
{ id: 2, name: '项目 2' }
])
let nextId = 3
function addItem() {
if (!newItem.value.trim()) return
items.value.push({ id: nextId++, name: newItem.value })
newItem.value = ''
}
function removeItem(id) {
const idx = items.value.findIndex(i => i.id === id)
if (idx >= 0) items.value.splice(idx, 1)
}
return {
showFade, showSlide, showModal, currentTab, tabs,
newItem, items, addItem, removeItem
}
}
}
createApp(App).mount('#app')
</script>
▶ 示例:6 种 CSS class 详解
| Class | 触发 |
|---|---|
.fade-enter-from |
元素插入前 |
.fade-enter-active |
元素插入过程 |
.fade-enter-to |
元素插入后 |
.fade-leave-from |
元素离开前 |
.fade-leave-active |
元素离开过程 |
.fade-leave-to |
元素离开后 |
▶ 示例:5 大第三方动画库(⚠️ 需 Vite)
| 库 | 命令 | 适用 |
|---|---|---|
| CSS | 无(内置) | 简单 |
| Animate.css | npm i animate.css |
预设动画 |
| @vueuse/motion | npm i @vueuse/motion |
声明式 |
| GSAP | npm i gsap |
复杂时间线 |
| Motion One | npm i motion |
轻量 Web 动画 |
▶ 示例:5 个常见错误速查
| 错误 | 现象 | 解决 |
|---|---|---|
| Transition 不生效 | 元素直接消失 | 加 <Transition> 包裹 + v-if |
| v-for 不动画 | 列表项没过渡 | 用 <TransitionGroup> |
| 排序没动画 | 移动突兀 | 加 .list-move class |
| 多个组件同时切换 | 重叠 | 加 key 区分 + mode="out-in" |
| CSS 动画太快/太慢 | 体验差 | 调整 transition duration 0.2-0.5s |
▶ 示例:5 大实战场景
| 场景 | 动画 |
|---|---|
| Tab 切换 | fade + out-in |
| Modal 弹窗 | scale + fade |
| 列表添加/删除 | list 动画 |
| 路由切换 | fade + slide |
| 通知提示 | slide from top + auto-dismiss |
❓ 常见问题
Q Transition 和 CSS @keyframes 怎么选?
A 简单过渡(opacity/transform)用 CSS transition。复杂动画(多步骤、物理)用 @keyframes 或 GSAP。Vue Transition 支持两种。
Q TransitionGroup 必须用 :key 吗?
A 是的。
<TransitionGroup> 子元素必须用 :key 唯一标识,否则动画不触发。Q 怎么实现"页面切换"动画?
A 用
<router-view v-slot="{ Component }"> + <Transition> 包裹。每次路由变化,Component 会变化,触发 Transition。Q mode="out-in" 有什么用?
A 让离开的组件先完成动画,再让进入的组件开始。避免两个组件同时显示(重叠)。
Q JS 钩子和 CSS class 能混用吗?
A 能。
<Transition :css="false" @enter="onEnter"> 禁用 CSS class,只用 JS 钩子。或两者都启用(用 CSS class 做样式,JS 钩子做回调)。Q 动画性能差怎么办?
A (1) 用 transform 和 opacity(GPU 加速);(2) 避免 layout 触发的属性(width/height/top);(3) 减少同时动画的元素数量。
Q Animate.css 怎么配合 Vue?
A 用
enter-active-class="animate__animated animate__fadeIn",把 Animate.css 的 class 名字映射到 Vue 的钩子。📖 小节
<Transition>单元素进入/离开,6 种 CSS class 自动管理<TransitionGroup>列表动画,.list-move处理排序- 3 种 mode:默认(重叠)/ out-in(推荐)/ in-out
- 5 种 CSS 动画:淡入淡出 / 滑动 / 缩放 / 旋转 / 组合
- JS 钩子 8 个:before-enter / enter / after-enter / before-leave / leave / after-leave
- 5 大第三方库:CSS / Animate.css / @vueuse/motion / GSAP / Motion One
- 5 大实战:Tab 切换 / Modal / 列表 / 路由 / 通知
📝 作业
-
基础题(难度⭐) 实现一个简单的淡入淡出 Modal:
- 点击按钮打开 Modal
- 淡入 0.3 秒
- 点击关闭淡出
- 背景遮罩同步淡入淡出
-
进阶题(难度⭐⭐) 实现一个 TransitionGroup 列表动画:
- 5 个待办事项
- 添加新项淡入(0.5 秒)
- 删除项淡出(0.5 秒 + position absolute)
- 排序时其他项平滑移动(.list-move)
-
挑战题(难度⭐⭐⭐) 实现完整的"页面切换动画"系统:
- 路由切换时 fade + slide 动画
- 列表添加/删除/排序的 3 种动画
- Modal 弹窗的 scale + fade 组合
- GSAP 集成的复杂时间线动画
- 5 个第三方动画库对比测试
- 性能优化(GPU 加速 + will-change)