Transition
关于状态变化的过渡和动画Vue
提供了两个内置组件<Transition>
和 <TransitionGroup>
,可以帮助制作基于状态变化的过渡和动画。
进入或者离开的动画
- 由 v-if 所触发的切换
- 由 v-show 所触发的切换
- 由特殊元素
<component>
切换的动态组件
- 改变特殊的 key 属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <script lang="ts" setup> import { ref } from 'vue' const show = ref(true) </script> <template> <div class="container"> <button @click="show = !show">点击切换按钮状态</button> <Transition> <p v-if="show">hello</p> </Transition> </div> </template> <style lang="scss" scoped> .container { .v-enter-active, .v-leave-active { transition: opacity 0.5s ease; } .v-enter-from, .v-leave-to { opacity: 0; } } </style>
|
<Transition>
仅支持单个元素或组件作为其插槽内容。如果内容是一个组件,这个组件必须仅有一个根元素。
当一个<Transition>
组件中的元素被插入或移除时,会发生下面这些事情:
Vue
会自动检测目标元素是否应用了CSS
过渡或动画。如果是,则一些 CSS
过渡 class
会在适当的时机被添加和移除。
如果有作为监听器的 JavaScript 钩子,这些钩子函数会在适当时机被调用。
如果没有探测到 CSS
过渡或动画、也没有提供JavaScript
钩子,那么 DOM
的插入、删除操作将在浏览器的下一个动画帧后执行。
基于 CSS
的过渡效果

v-enter-from:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。
v-enter-active:进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。
v-enter-to:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是 v-enter-from 被移除的同时),在过渡或动画完成之后移除。
v-leave-from:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。
v-leave-active:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。
v-leave-to:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是 v-leave-from 被移除的同时),在过渡或动画完成之后移除。
为过渡效果命名
我们可以给 Transition 组件传一个 name prop 来声明一个过渡效果名,后期动画前缀替换掉v
进行编写:
1 2 3
| <Transition name="fade"> ... </Transition>
|
如上,对于一个有名字的过渡效果,对它起作用的过渡 class 会以其名字而不是 v 作为前缀。这个“fade”过渡的 class 应该是这样:
1 2 3 4 5 6 7 8 9
| .fade-enter-active, .fade-leave-active { transition: opacity 0.5s ease; } .fade-enter-from, .fade-leave-to { opacity: 0; }
|
CSS
的 transition
Transition
一般都会搭配原生 CSS
过渡一起使用,正如你在上面的例子中所看到的那样。
这个 transition CSS
属性是一个简写形式,使我们可以一次定义一个过渡的各个方面,包括需要执行动画的属性、持续时间和速度曲线。
使用不同的持续时间和速度曲线来过渡多个属性示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <script lang="ts" setup> import { ref } from 'vue' const show = ref(true) </script> <template> <div class="container"> <button @click="show = !show">点击切换按钮状态</button> <Transition name="slide-fade"> <p v-if="show">持续时间和速度曲线控制</p> </Transition> </div> </template> <style lang="scss" scoped> .container {
.slide-fade-enter-active { transition: all 0.3s ease-out; } .slide-fade-leave-active { transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1); } .slide-fade-enter-from, .slide-fade-leave-to { transform: translateX(20px); opacity: 0; } } </style>
|
CSS
的 animation
原生 CSS
动画和 CSS transition
的应用方式基本上是相同的,只有一点不同,那就是 *-enter-from
不是在元素插入后立即移除,而是在一个 animationend
事件触发时被移除。
对于大多数的 CSS
动画,我们可以简单地在 _-enter-active
和 _-leave-active class
下声明它们。下面是一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <script lang="ts" setup> import { ref } from 'vue' const show = ref(true) </script> <template> <div class="container"> <button @click="show = !show">点击切换状态</button> <Transition name="bounce"> <p v-if="show" style="text-align: center">这是 CSS transition 示例</p> </Transition> </div> </template> <style lang="scss" scoped> .container { text-align: center; .bounce-enter-active { animation: bounce-in 0.5s; } .bounce-leave-active { animation: bounce-in 0.5s reverse; } @keyframes bounce-in { 0% { transform: scale(0); } 50% { transform: scale(1.25); } 100% { transform: scale(1); } } } </style>
|
自定义过渡 class
你也可以向 <Transition>
传递以下的 props 来指定自定义的过渡 class:
enter-from-class
enter-active-class
enter-to-class
leave-from-class
leave-active-class
leave-to-class
你传入的这些 class 会覆盖相应阶段的默认 class 名。这个功能在你想要在 Vue
的动画机制下集成其他的第三方CSS
动画库时非常有用,比如 Animate.css
:
安装 animate.css
动画库
1
| pnpm install animate.css --save
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <!-- 假设你已经在页面中引入了 Animate.css --> <script lang="ts" setup> import { ref } from 'vue' import 'animate.css' const show = ref(true) </script> <template> <div class="container"> <button @click="show = !show">点击切换状态</button> <Transition name="custom-classes" enter-active-class="animate__animated animate__zoomIn" leave-active-class="animate__animated animate__zoomOut" > <p v-if="show">引入animate动画库</p> </Transition> </div> </template> <style lang="scss" scoped> .container { text-align: center; } </style>
|
注意:animate__animated
是 Animate.css
库中的一个基础类,它本身并不直接提供动画效果,而是作为所有其他动画类的基础。这个类确保了动画能够平滑地运行,并且通常与具体的动画类(如 animate__zoomIn
或 animate__zoomOut
)一起使用来创建所需的动画效果。为了确保动画的兼容性和一致性,建议总是将它与具体的动画类一起使用。
同时使用 transition 和 animation
Vue
需要附加事件监听器,以便知道过渡何时结束。可以是 transitionend
或 animationend
,这取决于你所应用的 CSS
规则。如果你仅仅使用二者的其中之一,Vue
可以自动探测到正确的类型。
然而在某些场景中,你或许想要在同一个元素上同时使用它们两个。举例来说,Vue
触发了一个 CSS
动画,同时鼠标悬停触发另一个 CSS
过渡。此时你需要显式地传入 type prop 来声明,告诉 Vue
需要关心哪种类型,传入的值是 animation 或 transition:
1
| <Transition type="animation">...</Transition>
|
深层级过渡与显式过渡时长
尽管过渡 class 仅能应用在<Transition>
的直接子元素上,我们还是可以使用深层级的 CSS 选择器,在深层级的元素上触发过渡效果。
1 2 3 4 5 6 7
| <Transition name="nested"> <div v-if="show" class="outer"> <div class="inner"> Hello </div> </div> </Transition>
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| .nested-enter-active .inner, .nested-leave-active .inner { transition: all 0.3s ease-in-out; } .nested-enter-from .inner, .nested-leave-to .inner { transform: translateX(30px); opacity: 0; }
|
我们甚至可以在深层元素上添加一个过渡延迟,从而创建一个带渐进延迟的动画序列:
1 2 3 4
| .nested-enter-active .inner { transition-delay: 0.25s; }
|
然而,这会带来一个小问题。默认情况下,<Transition>
组件会通过监听过渡根元素上的第一个 transitionend
或者 animationend
事件来尝试自动判断过渡何时结束。而在嵌套的过渡中,期望的行为应该是等待所有内部元素的过渡完成。
在这种情况下,你可以通过向 <Transition>
组件传入 duration prop
来显式指定过渡的持续时间 (以毫秒为单位)。总持续时间应该匹配延迟加上内部元素的过渡持续时间:
1
| <Transition :duration="550">...</Transition>
|
如果有必要的话,你也可以用对象的形式传入,分开指定进入和离开所需的时间:
1
| <Transition :duration="550">...</Transition>
|
性能考量
你可能注意到我们上面例子中展示的动画所用到的 CSS
属性大多是 transform 和 opacity 之类的。用这些属性制作动画非常高效,因为:
他们在动画过程中不会影响到 DOM 结构,因此不会每一帧都触发昂贵的 CSS
布局重新计算。
大多数的现代浏览器都可以在执行 transform 动画时利用 GPU
进行硬件加速。
相比之下,像 height 或者 margin 这样的属性会触发 CSS
布局变动,因此执行它们的动画效果更昂贵,需要谨慎使用。我们可以在 CSS-Triggers 这类的网站查询哪些属性会在执行动画时触发 CSS
布局变动。
JavaScript 钩子
你可以通过监听<Transition>
组件事件的方式在过渡过程中挂上钩子函数:
1 2 3 4 5 6 7 8 9 10 11 12
| <Transition @before-enter="onBeforeEnter" @enter="onEnter" @after-enter="onAfterEnter" @enter-cancelled="onEnterCancelled" @before-leave="onBeforeLeave" @leave="onLeave" @after-leave="onAfterLeave" @leave-cancelled="onLeaveCancelled" > <!-- ... --> </Transition>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
function onBeforeEnter(el) {}
function onEnter(el, done) { done() }
function onAfterEnter(el) {}
function onEnterCancelled(el) {}
function onBeforeLeave(el) {}
function onLeave(el, done) { done() }
function onAfterLeave(el) {}
function onLeaveCancelled(el) {}
|
这些钩子可以与 CSS
过渡或动画结合使用,也可以单独使用。
在使用仅由 JavaScript 执行的动画时,最好是添加一个 :css="false" prop
。这显式地向 Vue
表明可以跳过对 CSS
过渡的自动探测。除了性能稍好一些之外,还可以防止CSS
规则意外地干扰过渡效果。
1 2 3 4 5 6
| <Transition ... :css="false" > ... </Transition>
|
在有了 :css="false"
后,我们就自己全权负责控制什么时候过渡结束了。这种情况下对于 @enter 和 @leave 钩子来说,回调函数 done 就是必须的。否则,钩子将被同步调用,过渡将立即完成。
这里是使用 GreenSock
库(https://gsap.com/)执行动画的一个示例,你也可以使用任何你想要的库,比如 Anime.js
(https://animejs.com/) 或者 Motion One(https://motion.dev/)。
可复用过渡效果
得益于 Vue
的组件系统,过渡效果是可以被封装复用的。要创建一个可被复用的过渡,我们需要为 <Transition>
组件创建一个包装组件,并向内传入插槽内容:
隐藏面板的交互
在 Vue 3
中实现一个隐藏面板并带有边缘图标的交互效果。(完整)
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| <template> <div id="app"> <!-- 图标按钮 --> <div class="toggle-button" :class="{ expanded: isPanelVisible }" @click="togglePanel" > <span v-if="isPanelVisible">-</span> <span v-else>+</span> <!-- 用element-plus的icon图标 --> <!-- <el-icon v-if="isPanelVisible"><DArrowRight /></el-icon>--> <!-- <el-icon v-else><DArrowLeft /></el-icon>--> </div>
<!-- 面板 --> <transition name="slide"> <div v-if="isPanelVisible" class="side-panel"> <h3>侧边面板</h3> <p>这里是侧边面板的内容。</p> </div> </transition> </div> </template>
<script lang="ts" setup> import { ref } from "vue";
// 控制面板是否可见 const isPanelVisible = ref(false);
// 切换面板状态 const togglePanel = () => { isPanelVisible.value = !isPanelVisible.value; }; </script>
<style lang="scss" scoped> /* 全局样式 */ #app { position: relative; height: 100vh; }
/* 面板样式 */ .side-panel { position: fixed; top: 0; right: 0; width: 300px; /* 面板宽度 */ height: 100%; background-color: #f9fafb; box-shadow: -5px 0 10px rgba(0, 0, 0, 0.1); padding: 20px; z-index: 1000; }
/* 图标按钮样式 */ .toggle-button { position: fixed; top: 50%; right: 0; transform: translate(0%, -50%); width: 20px; height: 50px; background-color: rgba(66, 185, 131, 0.5); /* 半透明背景色 */ color: rgba(255, 255, 255, 0.7); /* 半透明文字 */ display: flex; justify-content: center; align-items: center; font-size: 14px; cursor: pointer; border-radius: 4px 0 0 4px; transition: all 0.3s ease; /* 平滑过渡效果 */ z-index: 1001; }
.toggle-button:hover { width: 40px; background-color: #42b983; /* 完整背景色 */ color: white; /* 完整文字颜色 */ font-size: 18px; /* 显示文字 */ justify-content: center; /* 文字靠左对齐 */ }
/* 面板显示动画 */ .slide-enter-active, .slide-leave-active { transition: transform 0.3s ease; }
.slide-enter-from, .slide-leave-to { transform: translateX(100%); }
/* 当面板展开时,按钮的样式 */ .toggle-button.expanded { background-color: rgba(66, 185, 131, 0.5); /* 半透明背景色 */ color: white; } </style>
|
实现逻辑
- 面板的显示与隐藏:
- 使用
Vue
的 v-if
指令动态控制面板的渲染。
- 配合
CSS
动画(transition
),使面板以滑动的方式显示或隐藏。
- 图标按钮的功能:
- 图标按钮固定在页面右侧中间位置。
- 点击按钮时调用
togglePanel
方法,切换 isPanelVisible
的值。
- 动画效果:
- 使用
Vue
的 <transition>
标签为面板添加滑动动画。
- 在
slide-enter-active
和 slide-leave-active
中定义过渡效果。
- 图标状态变化:
- 当面板展开时,图标颜色可以改变,提示用户当前状态。