Vue provides a simple and elegant way to handle animations. We can <transition />
easily apply them by adding a directive that does all the heavy lifting for us. Alternatively, you can leverage javascript hooks to incorporate more complex logic into your animations, and even add gsap
third-party libraries like this one for more advanced usage scenarios.
In this post, we'll explore these different options, but first, let's put Vue aside for a moment and talk about the difference between CSS transitions and animations.
Transition Effects Vs Animation Effects
Transitions are made between two different states. start state and end state. Just like modal components, the start state may be hidden and the end state may be visible. With these states set, the browser fills in this state change with a series of intermediate frames.
button { background-color: #0ff1ce; transition: background-color 0.3s ease-in; } button:hover { background-color: #c0ffee; }
If you want to perform something that doesn't explicitly involve a start state and an end state, or if you need more fine-grained control over the keyframes in the transition, then you have to use animation.
If you want to perform something that doesn't explicitly involve a start state and an end state, or if you need more fine-grained control over the keyframes in the transition, then you have to use animation .
button:hover { animation-duration: 3s; animation-iteration-count: infinite; animation-name: wobble; } @keyframes wobble { 0%, 100% { transform: translateX(0%); transform-origin: 50% 50%; } 15% { transform: translateX(-32px) rotate(-6deg); } 30% { transform: translateX(16px) rotate(6deg); } 45% { transform: translateX(-16px) rotate(-3.6deg); } 60% { transform: translateX(10px) rotate(2.4deg); } 75% { transform: translateX(-8px) rotate(-1.2deg); } }
will result in the following:
If you consider that many properties can be animated, that multiple animations can be applied to an element, and that they can be controlled using javascript, the animation possibilities are endless.
Vue.js transition directive
In Vue.js projects, we can easily use transitions and animations by using the built-in transition directives. During the animation, Vue will add the appropriate class to the enclosing element.
Transition Classes
Enter
- v-enter-from : initial state.
- v-enter-active : Active state. Applied throughout animation stages.
- v-enter-to : End state.
leave
- v-leave-from : initial state.
- v-leave-active : Active state of leave. Applied throughout the animation stage.
- v-leave-to : End state.
In the case of named transitions, the name will replace v-
the prefix.
At first, it was a bit confusing for me, but as we dig into the code, everything becomes easier to understand. Let's start with an example.
animation example
Some trivial parts of the markup are omitted for brevity, but everything including a live demo is available on github.
Toggle with fade animation
<button @click="toggle">Toggle</button> <transition name="fade"> <div class="box" v-if="!isHidden"></div> </transition>
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; } .fade-enter-from, .fade-leave-to { opacity: 0; }
Toggle with sliding animation
.slide-enter-active { transition-duration: 0.3s; transition-timing-function: ease-in; } .slide-leave-active { transition-duration: 0.3s; transition-timing-function: cubic-bezier(0, 1, 0.5, 1); } .slide-enter-to, .slide-leave-from { overflow: hidden; } .slide-enter-from, .slide-leave-to { overflow: hidden; height: 0; }
switch between two buttons
<transition name="fade" mode="out-in"> <button @click="toggle" v-if="!isHidden" key="first">First State</button> <button @click="toggle" v-else key="second">Second State</button> </transition>
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; } .fade-enter-from, .fade-leave-to { opacity: 0; }
Toggle between two states
.bounce-enter-active { animation: bounce 0.3s; } .bounce-leave-active { animation: bounce 0.3s reverse; } @keyframes bounce { 0% { transform: scale(1); opacity: 0; } 60% { transform: scale(1.1); } 100% { transform: scale(1); opacity: 1; } }
List addition, deletion and shuffling
.list-enter-active, .list-leave-active { transition: all 0.3s; } .list-enter-from, .list-leave-to { opacity: 0; transform: scale(0); } /* Shuffle */ .list-move { transition: transform 0.6s; }
Modal mode
.modal-enter-from { opacity: 0; } .modal-leave-active { opacity: 0; } .modal-enter-from .modal-container, .modal-leave-active .modal-container { -webkit-transform: scale(1.1); transform: scale(1.1); }
card animation
/* moving */ .slideLeft-move { transition: all 0.6s ease-in-out 0.05s; } /* appearing */ .slideLeft-enter-active { transition: all 0.4s ease-out; } /* disappearing */ .slideLeft-leave-active { transition: all 0.2s ease-in; position: absolute; z-index: 0; } /* appear at / disappear to */ .slideLeft-enter-from, .slideLeft-leave-to { opacity: 0; }
Expand/collapse animation
.list-enter-active, .list-leave-active { transition: all 0.5s; } .list-enter-from, .list-leave-to { opacity: 0; height: 0; }
progress animation
<div class="progress-steps"> <div class="progress"> <div class="percent" :style="{width: `${ (progress-1) * 30 }%`}"></div> </div> <div class="steps"> <div class="step" v-for="index in 4" @click="setProgress(index)" :key="index" :class="{'selected': progress === index, 'completed': progress > index }"></div> </div> </div>
.container { position: relative; margin-top: 100px; } .progress-steps { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .steps { position: relative; display: flex; justify-content: space-between; width: 200px; } .step { width: 20px; height: 20px; background: #ffffff; border: 2px solid lightgray; border-radius: 50%; transition: all 0.6s; cursor: pointer; } .step.selected { border: 2px solid #42b983; } .step.completed { border: 2px solid #42b983; background: #42b983; border-radius: inherit; } .step.completed:before { font-family: "FontAwesome"; color: white; content: "\f00c"; } .progress { position: absolute; width: 100%; height: 50%; border-bottom: 2px solid lightgray; z-index: -1; } .percent { position: absolute; width: 0; height: 100%; border-bottom: 2px solid #42b983; z-index: 1; transition: width 0.6s; }
navigation animation
... methods: { navigateTo(item) { const previousItem = this.$refs[`Item_${this.currentRoute.id}`]; const nextItem = this.$refs[`Item_${item.id}`]; this.currentRoute = item; this.animateOut(previousItem); this.animateIn(nextItem); }, animateIn(item) { this.tweenColor(item, { backgroundColor: this.currentRoute.color, color: "white" }); this.tweenSize(item, 64); }, animateOut(item) { this.tweenColor(item, { backgroundColor: "white", color: "gray" }); this.tweenSize(item, 32); }, tweenColor(item, options) { TweenMax.to(item, 0.3, options); }, tweenSize(item, width) { TweenMax.to(item, 0.7, { width, ease: Elastic.easeOut.config(1, 0.5) }); } } ...
Differences from Vue 2
Animations are one of many features affected by the Vue 3 migration. The migration build doesn't report this as a breaking change, which can be confusing.
The old class looked like this:
As you can see, the .v-enter
and .v-leave
classes are now replaced by .v-enter-from
and .v-leave-from
. Also, the transition element property that controls the animation class name has changed from enter-class
and to and .leave-class
enter-class-from
leave-class-from
Vue provides a powerful way to incorporate simple or complex animations into our applications. When used correctly, they can enhance the overall user experience and make the interface more natural and professional. However, there is always a balance to be found, as too much animation can have the opposite effect. So, make sure not to overdo it.