学完这篇,你能把 Vue 3 的 <Transition> 从「加个 fade」用到「多元素切换不乱、列表增删有动画、时长由数据决定、和 GSAP 这类动画库打通」。
下面按从易到难的顺序拆,每一步都给出可直接粘贴的代码和参数。
第一步:先把基础结构写对
<Transition> 只能包单个根元素(或单个根组件),并且要给它配一个 name,Vue 会据此生成 6 个类名:xxx-enter-from、xxx-enter-active、xxx-enter-to、xxx-leave-from、xxx-leave-active、xxx-leave-to。
<template>
<Transition name="fade">
<div v-if="show" class="box">内容</div>
</Transition>
</template>
<style>
.fade-enter-active, .fade-leave-active { transition: opacity .3s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>
进场的耗时主要由 enter-active 决定,from 是初始态、to 是结束态;离场反过来。写反了就会看到「瞬间消失」,这是最常见的第一个坑。
注意:<Transition> 内的元素必须靠 v-if / v-show / 动态组件 / <component :is> 切换,直接改内容文本不会触发动画。
第二步:多元素切换必须加 key 和 mode
同时写 v-if 和 v-else 的两个兄弟元素,或者用 :key 切换同一元素的不同状态时,Vue 默认会让进场和离场同时发生,于是出现两个块重叠、高度跳变。
<Transition name="slide" mode="out-in">
<component :is="currentTab" :key="currentTab" />
</Transition>
mode="out-in":旧的先走完,新的再进(最常用于标签页、路由)。
mode="in-out":新的先进,旧的后走(少见,容易看起来很挤)。
同标签名的两个元素(比如两个 <div>)一定要加不同的 :key,否则 Vue 会认为是同一个节点,直接复用、不播动画。
第三步:给列表用 TransitionGroup
<TransitionGroup> 用于 v-for 列表,多一个 move 类(元素位置变化时触发),并且默认不渲染包裹元素,需要你指定 tag。
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">{{ item.text }}</li>
</TransitionGroup>
<style>
.list-move, .list-enter-active, .list-leave-active {
transition: all .35s cubic-bezier(.4, 0, .2, 1);
}
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(20px); }
/* 关键:离场元素脱离文档流,剩下的才能平滑上移 */
.list-leave-active { position: absolute; }
</style>
注意:key 必须稳定且唯一,别用数组下标。用 index 做 key,删除中间项时后面所有元素的 key 都会变,动画会错乱。
第四步:用 JS 钩子接管动画时机
CSS 动画搞不定时(比如要等接口、要串行动画),改用钩子。<Transition> 支持 @before-enter、@enter、@after-enter、@leave 等,@enter 和 @leave 会收到 (el, done),必须调用 done(),否则 Vue 会一直等,元素卡住不消失。
<Transition
@enter="(el, done) => {
gsap.fromTo(el, { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: .4, onComplete: done })
}"
@leave="(el, done) => {
gsap.to(el, { opacity: 0, y: -20, duration: .3, onComplete: done })
}"
>
<div v-if="show">GSAP 驱动</div>
</Transition>
:css="false" 可以关掉 Vue 对 CSS 类名的自动探测,避免它自己推断时长(推断不准会导致 after-enter 提前触发)。
第五步:用 CSS 变量让时长跟着数据走
动画时长经常不是常量(列表越长越慢、用户设置里可调速度)。在元素上写 --dur,CSS 里用 var() 读:
<div v-if="show" :style="{ '--dur': duration + 'ms' }">…</div>
<style>
.fade-enter-active, .fade-leave-active { transition: opacity var(--dur, .3s) ease; }
</style>
这样不用为每种速度写一套类名,也不用改成 JS 钩子。
第六步:首屏渲染时禁用动画
页面首次加载时,appear 默认关闭,元素会直接出现——这通常是你要的。但如果你手动加了 appear,首屏就会集体播一遍入场动画,观感很差。需要时用 :appear="false" 显式关掉,或只在用户已交互后才开启。
注意:appear 用的是 appear-from / appear-active / appear-to 这套类名,不是 enter-*,想让首屏动画和普通入场一致,这两组类名都得写。
第七步:性能与可访问性收尾
- 只动
transform 和 opacity,别动 width / height / top / left,否则每帧都在重排。
- 大量元素同时动画时,
will-change: transform 适度加,加多了反而吃显存。
- 尊重系统偏好:在外面包一层
@media (prefers-reduced-motion: reduce) 把 transition-duration 设为 0.01ms,无障碍检查不会扣分。
@media (prefers-reduced-motion: reduce) {
* { transition-duration: .01ms !important; animation-duration: .01ms !important; }
}
小结
- 单元素用
<Transition>,列表用 <TransitionGroup tag="ul">,后者多一个 move 类。
- 多元素切换必配
:key + mode="out-in",否则两个块会重叠。
- 离场元素加
position: absolute 是列表平滑上移的关键。
- 用 JS 钩子时,
@enter / @leave 里的 done() 一定要调,配合 :css="false" 更稳。
- 时长交给 CSS 变量
--dur,别为每种速度复制一套类名。
- 只动
transform / opacity,并给 prefers-reduced-motion 留后路。