Vue3 过渡动画
美观的页面都不是冰冷的突然出现/突然消失的,一般都有”过渡”、”切换”的动画,弹窗淡入淡出、列表项滑入滑出、标签页切换的位移,这些都是过渡动画。
Vue 内置两个组件帮我们省掉大把 JS:<Transition>(单元素进出场)和 <TransitionGroup>(列表项增删移动)。
<Transition>
给要动画的元素套上 <Transition>(只能包一个元素),模板部分很普通,就是个按钮配 v-if 切换:
<script setup>
import { ref } from 'vue'
const show = ref(true)
</script>
<template>
<button @click="show = !show">切换显示</button>
<Transition>
<p v-if="show">这段文字会淡入淡出</p>
</Transition>
</template>
动画效果藏在 CSS 里,<Transition> 会监控内部元素的插入/移除(由 v-if、v-show、动态组件触发),并在合适的时机自动给元素加上 v-enter-* 和 v-leave-* 类,我们只需要声明过渡前状态和过渡期间的行为:
/* 整个过渡期间:给透明度变化设置时长和缓动 */
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
/* 过渡前的起点:刚进入时和要离开时,先把透明度设为 0 */
.v-enter-from,
.v-leave-to {
opacity: 0;
}
<Transition> 不能直接包多个,可以用 <template v-if="..."> 把元素组包起来;真正要”多个并行”的进出场,用多个 <Transition>,或直接用后面的 <TransitionGroup>。
动画结束后想接着做点事,可以用 @after-enter / @after-leave 等钩子,它们在 class 过渡结束后触发,适合做收尾处理(如展开面板后滚动定位),这类场景用得不多,遇到再查文档即可。
动画类名的规律
默认类名前缀是 v-,一共 6 个类,成对出现:
| 类名 | 时机 |
|---|---|
v-enter-from |
进入开始:元素刚被插入 |
v-enter-active |
进入过程:整个进入阶段的过渡效果 |
v-enter-to |
进入结束:目标状态 |
v-leave-from |
离开开始 |
v-leave-active |
离开过程 |
v-leave-to |
离开结束(元素随后被移除) |
给 <Transition> 一个 name,类名前缀就从 v- 变成名字:
<Transition name="fade">
<p v-if="show">淡入淡出</p>
</Transition>
<style scoped>
/* 注意前缀随 name 变成 fade- */
.fade-enter-active, .fade-leave-active { transition: opacity .3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>
包了 <Transition> 却看不到动画,八成是忘了写 CSS(transition 那几条),或 name 改了但 CSS 前缀没跟着改,或里面不是一个元素/组件(多个元素要外加一层)。
排查时先把 name 去掉,让类名回到默认的 v- 前缀,这样”前缀对不上”这个坑就没了,要是还没动画,基本就剩”忘了写 CSS”或”不是单个元素”。
控制进入/离开顺序:mode
默认”同时进出”可能出现瞬间两个元素叠着(旧先离、新又入)。想要”旧的先走光、新的再进来”,用 mode="out-in":
<Transition name="fade" mode="out-in">
<p v-if="show">左边内容</p>
<p v-else>右边内容</p>
</Transition>
注意这也解决了”<Transition> 内 v-if 替换的两个元素”只能出现一个的问题。
<TransitionGroup>
列表增删也要动起来,用 <TransitionGroup>,它渲染一个真实容器(默认 span),可以配 tag 改成渲染 ul 等,给出现的项加动画:
<script setup>
import { ref } from 'vue'
const list = ref([{ id: 1, name: '苹果' }, { id: 2, name: '香蕉' }])
const addItem = () => list.value.push({ id: Date.now(), name: '橙子' })
const removeItem = (id) => list.value = list.value.filter(i => i.id !== id)
</script>
<template>
<button @click="addItem">加一项</button>
<TransitionGroup name="list" tag="ul">
<li v-for="item in list" :key="item.id">
{{ item.name }}
<button @click="removeItem(item.id)">删除</button>
</li>
</TransitionGroup>
</template>
<style scoped>
.list-enter-active, .list-leave-active { transition: all 0.3s ease; }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(20px); }
</style>
两个细节:
- 列表项必须有
:key,TransitionGroup靠它识别每一项(和 v-for 的规则一致)。 - 想”移除后其余项平滑凑位”,给离开元素加
position: absolute(写进 leave-active 里),进阶时才需要。
速查卡片
| 场景 | 写法 |
|---|---|
| 单元素进退场 | <Transition> + CSS 类(默认前缀 v-) |
| 自定义类名前缀 | <Transition name="fade"> → .fade-enter-from 等 |
| 先出后进 | mode="out-in" |
| 列表增删 | <TransitionGroup name="list" tag="ul"> |
| 列表项 | 必须有 :key |
| 常用于 | 弹窗、Tab 切换、消息通知、折叠、列表增删 |