一、Vue动画靠什么?两个组件打天下
Vue 帮你封装好了两个组件:
-
<Transition>:给单个元素或组件添加进入/离开动画。 -
<TransitionGroup>:给列表里的多个元素添加进入/离开动画,还能加移动动画。
你只需要把要做动画的元素包在里面,然后给几个特定名字的 CSS 类写好样式,Vue 就会在合适的时机自动添加/移除这些类,动画就出来了。
用人话讲: 你告诉 Vue "这个元素出来时要淡入,走时要淡出",Vue 就帮你管什么时候加什么类,你只负责写好类里面的 CSS 动画。
二、最简单的淡入淡出
先看一个最基础的例子:点击按钮,一段文字淡入淡出。
vue
<template>
<div>
<button @click="show = !show">切换显示</button>
<!--
Transition 组件,name="fade" 就是给动画起个名字
Vue 会根据这个名字去匹配 CSS 类
-->
<Transition name="fade">
<!-- 只有这一个元素会被加上动画类 -->
<p v-if="show">我会淡入淡出</p>
</Transition>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 控制元素显示隐藏
const show = ref(false)
</script>
<style scoped>
/*
下面这几个类名是固定格式:name-enter-from、name-enter-active 等等
因为 Transition 上的 name 是 "fade",所以类名都以 "fade" 开头
*/
/* 进入的初始状态:完全透明 */
.fade-enter-from {
opacity: 0;
}
/* 进入的过程:过渡 0.5 秒,对 opacity 属性做动画 */
.fade-enter-active {
transition: opacity 0.5s ease;
}
/* 进入的结束状态:完全不透明(其实这个是默认值,可以不写) */
.fade-enter-to {
opacity: 1;
}
/* 离开的初始状态:完全不透明 */
.fade-leave-from {
opacity: 1;
}
/* 离开的过程:过渡 0.5 秒 */
.fade-leave-active {
transition: opacity 0.5s ease;
}
/* 离开的结束状态:完全透明 */
.fade-leave-to {
opacity: 0;
}
</style>
代码拆解:
-
<Transition name="fade">里的name是这个动画的名字,随便起,但别和别的冲突。 -
CSS 类名格式:
name-进入/离开的阶段。-
-enter-from:进入开始时的状态。 -
-enter-active:进入过程中的过渡效果(这里写transition)。 -
-enter-to:进入结束时的状态。 -
-leave-from:离开开始时的状态。 -
-leave-active:离开过程中的过渡效果。 -
-leave-to:离开结束时的状态。
-
-
我们只在
-enter-active和-leave-active里写了transition,告诉 Vue "当状态变化时,用 0.5 秒平滑过渡"。 -
v-if="show"切换时,Vue 会自动给这个<p>元素依次加上fade-enter-from、fade-enter-active、fade-enter-to这几个类。
三、其实可以更简洁:利用默认状态省略一些类
很多时候 -enter-to 和 -leave-from 就是元素本来的样子,不用专门写。上面的 CSS 可以精简为:
css
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
解释:
-
进入开始和离开结束都是透明(
opacity: 0)。 -
进入结束和离开开始都是不透明(默认),所以不用写。
-
只要在
-active里写好transition,Vue 就会自动补齐中间状态。
这样以后你写动画,基本就记住这两个类就行。
四、给弹窗加个滑入滑出效果
淡入淡出太普通?咱们试试从上面滑下来,消失时滑上去。
vue
<template>
<div>
<button @click="showModal = !showModal">切换弹窗</button>
<!-- 这次 name 叫 "slide" -->
<Transition name="slide">
<div v-if="showModal" class="modal">
<p>我是一个弹窗</p>
<button @click="showModal = false">关闭</button>
</div>
</Transition>
</div>
</template>
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>
<style scoped>
.modal {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: white;
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
/* 进入的初始状态:向上偏移 30px,且透明 */
.slide-enter-from {
opacity: 0;
transform: translateY(-30px);
}
/* 离开的结束状态:同样向上偏移并透明 */
.slide-leave-to {
opacity: 0;
transform: translateY(-30px);
}
/* 进入和离开的过程:过渡 0.3 秒 */
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s ease;
}
</style>
关键点:
-
transform: translateY(-30px)让元素在 Y 轴上移 30px。 -
配合
opacity,就形成了"从上方滑入并淡入"的效果。 -
离开时反过来,滑上去并淡出。
五、自定义过渡类名:用第三方动画库 Animate.css
自己写 CSS 动画有时候挺麻烦,社区有很多现成的动画库,比如 Animate.css ,里面有几十种预设动画(弹入、翻转、抖动等)。Vue 的 <Transition> 允许你自定义过渡的类名,直接用 Animate.css 的类名。
第一步: 在 index.html 里引入 Animate.css(或者 npm 安装导入)
html
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" />
第二步: 在组件里用
vue
<template>
<div>
<button @click="show = !show">切换动画</button>
<!--
关键属性:
enter-active-class 指定进入过程使用的类
leave-active-class 指定离开过程使用的类
这里直接写 Animate.css 的类名
-->
<Transition
enter-active-class="animate__animated animate__bounceIn"
leave-active-class="animate__animated animate__bounceOut"
>
<div v-if="show" class="box">
我会弹入弹出
</div>
</Transition>
</div>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(false)
</script>
<style scoped>
.box {
width: 200px;
height: 100px;
background: #42b983;
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
}
</style>
解释:
-
enter-active-class替代了之前的name-enter-active,直接指定进入时用的 CSS 类。 -
Animate.css 的类名格式是
animate__animated+ 具体动画名(如animate__bounceIn)。 -
这样你就能瞬间用上几十种预设动画,完全不用自己写关键帧。
六、给组件切换加过渡:mode 属性
有时候我们要在两个组件之间切换,比如登录页和注册页。默认情况下,一个组件离开和另一个组件进入是同时进行的,可能会有点视觉上的重叠。
<Transition> 有个 mode 属性,可以设置成:
-
out-in:当前元素先离开,完成后新元素再进入(推荐)。 -
in-out:新元素先进入,完成后旧元素再离开。
vue
<template>
<div>
<button @click="isLogin = !isLogin">
切换到 {{ isLogin ? '注册' : '登录' }}
</button>
<!-- mode="out-in" 保证旧组件完全离开后,新组件才进入 -->
<Transition name="switch" mode="out-in">
<LoginForm v-if="isLogin" key="login" />
<RegisterForm v-else key="register" />
</Transition>
</div>
</template>
<script setup>
import { ref } from 'vue'
import LoginForm from './LoginForm.vue'
import RegisterForm from './RegisterForm.vue'
const isLogin = ref(true)
</script>
<style scoped>
.switch-enter-from,
.switch-leave-to {
opacity: 0;
transform: translateX(20px);
}
.switch-enter-active,
.switch-leave-active {
transition: all 0.3s ease;
}
</style>
重点:
-
两个组件用
v-if/v-else切换。 -
别忘了加
key,不然 Vue 会复用组件,动画可能失效。 -
mode="out-in"让切换更流畅。
七、列表动画:TransitionGroup
前面都是单个元素的进入/离开。列表呢?比如购物车删除一项,其他项往上移动时能不能也加个动画?用 <TransitionGroup>。
vue
<template>
<div>
<button @click="addItem">添加一项</button>
<button @click="removeItem">删除最后一项</button>
<!--
TransitionGroup 组件,tag="ul" 表示渲染成 ul 标签
name="list" 给动画起名
-->
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id" class="list-item">
{{ item.text }}
<button @click="removeSpecific(item.id)">删除</button>
</li>
</TransitionGroup>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 初始数据
const items = ref([
{ id: 1, text: '第一项' },
{ id: 2, text: '第二项' },
{ id: 3, text: '第三项' }
])
let nextId = 4
function addItem() {
items.value.push({ id: nextId++, text: `第${nextId - 1}项` })
}
function removeItem() {
items.value.pop()
}
function removeSpecific(id) {
items.value = items.value.filter(item => item.id !== id)
}
</script>
<style scoped>
.list-item {
padding: 10px;
margin: 5px;
background: #f9f9f9;
border: 1px solid #ddd;
border-radius: 4px;
list-style: none;
}
/* 进入和离开的动画 */
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
/*
这个类很特殊:list-move
当列表里的元素因为其他元素的位置变化而移动时,
Vue 会给它加上这个类,让你能添加平滑移动动画
*/
.list-move {
transition: transform 0.5s ease;
}
</style>
解释:
-
<TransitionGroup>会渲染成真实的标签(这里tag="ul")。 -
每个
v-for出来的元素必须有唯一的key。 -
.list-move这个类用来处理其他元素移动 时的过渡,让列表变化更平滑。比如删除一项,下面所有项会往上移,这个过程会加上list-move的过渡。
八、实战:带过渡动画的待办事项列表
我们综合一下,做一个完整的待办事项,包含添加、完成(删除)、全部清空,并且有滑动淡入淡出效果。
vue
<template>
<div class="todo-app">
<h2>待办事项</h2>
<!-- 输入框和添加按钮 -->
<div class="input-group">
<input
v-model="newTodoText"
@keyup.enter="addTodo"
placeholder="输入待办内容,回车添加"
/>
<button @click="addTodo">添加</button>
</div>
<!-- 待办列表,使用 TransitionGroup -->
<TransitionGroup name="todo" tag="ul" class="todo-list">
<li
v-for="todo in todos"
:key="todo.id"
class="todo-item"
>
<span :class="{ done: todo.completed }">{{ todo.text }}</span>
<div>
<button @click="toggleComplete(todo.id)">
{{ todo.completed ? '撤销' : '完成' }}
</button>
<button @click="removeTodo(todo.id)">删除</button>
</div>
</li>
</TransitionGroup>
<!-- 空状态提示 -->
<p v-if="todos.length === 0" class="empty">暂无待办,添加一条吧</p>
<!-- 底部操作 -->
<div v-if="todos.length > 0" class="footer">
<span>共 {{ todos.length }} 项</span>
<button @click="clearCompleted">清除已完成</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const newTodoText = ref('')
const todos = ref([])
let nextId = 1
function addTodo() {
const text = newTodoText.value.trim()
if (!text) return // 空内容不添加
todos.value.push({
id: nextId++,
text,
completed: false
})
newTodoText.value = '' // 清空输入框
}
function removeTodo(id) {
todos.value = todos.value.filter(todo => todo.id !== id)
}
function toggleComplete(id) {
const todo = todos.value.find(todo => todo.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
function clearCompleted() {
todos.value = todos.value.filter(todo => !todo.completed)
}
</script>
<style scoped>
.todo-app {
max-width: 500px;
margin: 0 auto;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.input-group input {
flex: 1;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
margin-bottom: 8px;
background: #f5f5f5;
border-radius: 4px;
}
.todo-item .done {
text-decoration: line-through;
color: #999;
}
.empty {
text-align: center;
color: #999;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #eee;
}
/* 待办项的进入/离开动画 */
.todo-enter-from {
opacity: 0;
transform: translateX(-20px);
}
.todo-leave-to {
opacity: 0;
transform: translateX(20px);
}
.todo-enter-active,
.todo-leave-active {
transition: all 0.4s ease;
}
/* 其他元素移动时的平滑过渡 */
.todo-move {
transition: transform 0.4s ease;
}
</style>
效果:
-
添加事项时,新项从左侧滑入并淡入。
-
删除时,项向右侧滑出并淡出。
-
完成的事项会加删除线,但不影响动画。
-
下方元素上移时平滑过渡。
九、几个常见坑和技巧
1. 必须有 key
-
在
<Transition>里用v-if/v-else切换多个元素时,每个元素要加key,不然 Vue 会复用 DOM,动画可能不生效。 -
在
<TransitionGroup>里,v-for的每个项必须有唯一key。
2. 元素不要设置 display: none
- 动画依赖元素的尺寸和位置,如果加了
display: none,Vue 可能获取不到初始状态,动画会失效。
3. 过渡模式用 out-in 更流畅
- 组件切换时,
mode="out-in"能让上一个完全出去后,下一个再进来,避免重叠感。
4. 结合 JavaScript 钩子做复杂动画
- 除了 CSS,
<Transition>还支持@before-enter、@enter、@leave等 JavaScript 钩子,你可以用 GSAP 等库做更复杂的动画。这里先不展开,知道有这个能力就行。
十、总结
今天我们学会了:
-
<Transition>:给单个元素/组件加进入离开动画。 -
<TransitionGroup>:给列表元素加动画,还能处理元素移动。 -
CSS 类命名规则 :
name-enter-from、name-enter-active、name-leave-to等。 -
自定义类名:配合 Animate.css 等库直接用现成动画。
-
mode属性:控制多个元素切换时的顺序。
动画是用户体验的润滑剂,不用写太多,关键的地方加一点点,页面档次立马不一样。建议你把这些案例都敲一遍,尤其是待办事项那个,包含了大部分常用技巧,改成你自己的项目就能用。
有问题评论区说,我挨个回。下篇咱们聊 Vue 的组合式函数(Composables),把逻辑抽取得更优雅!
