Intro to Vue.js: Animations
This is the fifth part in a five-part series about the JavaScript framework, Vue.js. In this last part of the series, we’ll cover Animations (if you know me at all, you probably knew this was coming). This is not intended to be a complete guide, but rather an overview of the basics to get you up and running so you can get to know Vue.js and understand what the framework has to offer.
Article Series:
- Rendering, Directives, and Events
- Components, Props, and Slots
- Vue-cli
- Vuex
- Animations (You are here!)
Some background
There are built-in and
components that allow for both CSS and JS hooks. If you come from React, the concept behind the transition component will be familiar to you, because it works similarly to
ReactCSSTransitionGroup
in relationship to lifecycle hooks, but it has some notable differences that make nerds like me excited.
We’ll start off by talking about CSS Transitions, then move on to CSS Animations, then we’ll talk about JS Animation Hooks and then animating with Lifecycle Methods. Transitioning state is out of the scope of this article, but it is possible. Here’s a well-commented Pen I made that does just that. I could probably be convinced to write that article too, once I take a long nap.
Transitions vs. Animations
Just in case you’re confused by why Transitions and Animations have different sections in this article, let me explain that though they sound similar, they’re a bit different. A transition basically works by interpolating the values from state to another. We can do great things with them, but they are rather simple. Here, to there, and back again.
Animations are a bit different in that you can make multiple states occur within one declaration. For instance, you could set a keyframe 50% into the animation, and then another totally different thing can occur at 70%, and so on. You can even chain many animations with delays for really complex movement. Animations have the ability to behave like transitions, where we only interpolate something from here to there, but transitions can’t have multiple steps like an animation (not without some crazy hacky development that it’s not really supposed to be used for.)
In terms of tools, both are useful. Think of transitions as a saw and animations as a powersaw. Sometimes you just need to saw one thing and it would be silly to go out and buy really expensive equipment. For other more robust projects, it makes more sense to make the powersaw investment.
Now that we have those basics down, let’s talk about Vue!
CSS Transitions
Let’s say we have a simple modal. The modal shows and hides on a click of a button. Based on the previous sections, we already know that we might: make a Vue instance with a button, make a child component from that instance, set the data on the state so that it toggles some sort of boolean and add an event handler to show and hide this child component. We could use v-if
or v-show
to toggle the visibility. We might even use a slot to pass the button toggle into the modal as well.
<div id="app">
<h3>Let's trigger this here modal!</h3>
<button @click="toggleShow">
<span v-if="isShowing">Hide child</span>
<span v-else>Show child</span>
</button>
<app-child v-if="isShowing" class="modal">
<button @click="toggleShow">
Close
</button>
</app-child>
</div>
<script type="text/x-template" id="childarea">
<div>
<h2>Here I am!</h2>
<slot></slot>
</div>
</script>
const Child = {
template: '#childarea'
};
new Vue({
el: '#app',
data() {
return {
isShowing: false
}
},
methods: {
toggleShow() {
this.isShowing = !this.isShowing;
}
},
components: {
appChild: Child
}
});
See the Pen by Sarah Drasner.
This works, but it’s pretty jarring to have that modal just pop in our faces like that. ?
We’re already mounting and unmounting that child component with v-if
, so Vue will let us track changes on that event if we wrap that conditional in a transition component:
<transition name="fade">
<app-child v-if="isShowing" class="modal">
<button @click="toggleShow">
Close
</button>
</app-child>
</transition>
Now, we could just use out of the box. This will give us a
v-
prefix for some transition hooks we can use in our CSS. It will offer enter/leave
which is the position that the animation starts with on the first frame, enter-active/leave-active
while the animation is running- this is the one you’d place the animation properties themselves on, and enter-to/leave-to
, which specifies where the element should be on the last frame.
I’m going to use a graphic from the docs to show this because I think it describes the classes as beautifully and clearly as possible:
Personally, I don’t usually work with the default v-
prefix. I’ll always give the transition a name so that there are no collisions if I want to eventually apply another animation. It’s not hard to do so, as you can see above, we simply added a name
attribute to the transition component: name="fade"
.
Now that we have our hooks, we can create the transition using them:
.fade-enter-active, .fade-leave-active {
transition: opacity 0.25s ease-out;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
The .fade-enter-active
and .fade-leave-active
classes will be where we apply the actual transition. This is normal CSS, you can pass in cubic-beziers for eases, delays, or specify other properties to transition. Truthfully, this would also work just as well if you placed the transition in these classes on the component classes themselves as a default. These don’t necessarily need to be defined by the transition component hooks. They’ll just chill there, and wait until that property changes and use it to transition if it does. (so you would still need the transition component and .fade-enter, .fade-leave-to). The one reason I do use it on the enter-active and leave-active classes is that I can reuse the same transition for other elements as well, and not run around the codebase applying the same default CSS to each instance.
Another thing to note here: I’m using ease-out
for both active classes. This works and looks fine for something like opacity. But you may find that if you’re transitioning properties such as transform, you might want to separate the two and use ease-out
for the enter-active class and ease-in
for the enter-leave class (or cubic-beziers that vaguely follow the same curve). I find it makes the animation look more… classy (har har).
You can see we’ve also set the .fade-enter and the .fade-to to opacity: 0
. These will be the first and last positions of the animation, the initial state as it mounts, the end state as it unmounts. You may think you need to set opacity: 1
on .fade-enter-to
, and .fade-leave
, but that is unnecessary as it’s the default state for the component, so it would be redundant. CSS transitions and animations will always use the default state unless told otherwise.
See the Pen by Sarah Drasner.
This works nicely! But what would happen if we wanted to make that background content fade out of view, so that the modal took center stage and the background lost focus? We can’t use the component, as that component works based on something being mounted or unmounted, and the background is just sticking around. What we can do is transition classes based on the state, and use the classes to create CSS transitions that alter the background:
<div v-bind:class="[isShowing ? blurClass : '', bkClass]">
<h3>Let's trigger this here modal!</h3>
<button @click="toggleShow">
<span v-if="isShowing">Hide child</span>
<span v-else>Show child</span>
</button>
</div>
.bk {
transition: all 0.1s ease-out;
}
.blur {
filter: blur(2px);
opacity: 0.4;
}
new Vue({
el: '#app',
data() {
return {
isShowing: false,
bkClass: 'bk',
blurClass: 'blur'
}
},
...
});
See the Pen by Sarah Drasner.
CSS Animation
Now that we understand how transitions work, we can build off of those core concepts to create some nice CSS animations. We’ll still use the component, and we’ll still give it a name, allowing us to have the same class hooks. The difference here will be that instead of just setting the final state and saying how we want it to interpolate between beginning and end, we’ll use
@keyframes
in CSS to create fun and lovely effects.
In the last section, we talked a little about how you can designate a special name for the transition component that we can then use as class hooks. But in this section, we’ll go a step further, and apply different class hooks to different sections of the animation. You’ll recall that enter-active and leave-active is where all the juicy business of animating happens. We can set different properties on each of these class hooks, but we can go one step further and give special classes to each instance:
enter-active-class="toasty"
leave-active-class="bounceOut"
This means we can reuse those classes or even plug into the classes from CSS animation libraries.
Let’s say we want a ball to bounce in and roll out:
<div id="app">
<h3>Bounce the Ball!</h3>
<button @click="toggleShow">
<span v-if="isShowing">Get it gone!</span>
<span v-else>Here we go!</span>
</button>
<transition
name="ballmove"
enter-active-class="bouncein"
leave-active-class="rollout">
<div v-if="isShowing">
<app-child class="child"></app-child>
</div>
</transition>
</div>
For the bounce, we’d need a lot of keyframes if we want to do this in CSS (though in JS this could be one line of code), we also will use a SASS mixin to keep our styles DRY (don’t repeat yourself). We’ve also designated the .ballmove-enter
class to let the component know that it should start offscreen:
@mixin ballb($yaxis: 0) {
transform: translate3d(0, $yaxis, 0);
}
@keyframes bouncein {
1% { @include ballb(-400px); }
20%, 40%, 60%, 80%, 95%, 99%, 100% { @include ballb() }
30% { @include ballb(-80px); }
50% { @include ballb(-40px); }
70% { @include ballb(-30px); }
90% { @include ballb(-15px); }
97% { @include ballb(-10px); }
}
.bouncein {
animation: bouncein 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
.ballmove-enter {
@include ballb(-400px);
}
For rolling the ball out, you can see that we need to nest two different animations. This is because the transform is being applied to the entire child component, and spinning the whole thing would result in a huge rotation. So we’ll move the component across the screen with a translation, and spin the ball within with a rotation:
@keyframes rollout {
0% { transform: translate3d(0, 300px, 0); }
100% { transform: translate3d(1000px, 300px, 0); }
}
@keyframes ballroll {
0% { transform: rotate(0); }
100% { transform: rotate(1000deg); }
}
.rollout {
width: 60px;
height: 60px;
animation: rollout 2s cubic-bezier(0.55, 0.085, 0.68, 0.53) both;
div {
animation: ballroll 2s cubic-bezier(0.55, 0.085, 0.68, 0.53) both;
}
}
See the Pen Ball Bouncing using Vue transition and CSS Animation by Sarah Drasner (@sdras) on CodePen.
Sweet, Sweet Transition Modes
Do you recall when I said that Vue offers some really nice sugary bits in transitions that make nerds like me happy? Here’s a small one that I really love. If you try to transition one component in while another component is leaving, you’ll end up with this really weird moment where both exist at the same time and then snap back into place (this small example from the Vue docs):
Vue offers transition modes, which will allow you to transition one component out while bringing another component in without any strange position flashing or blocking. It does so by ordering the transitioning instead of having them occur at the same time. There are two modes to choose from:
In-out: The current element waits until the new element is done transitioning in to fire
Out-in: The current element transitions out and then the new element transitions in.
Check out the demo below. You can see the mode- out-in
on the transition component so that it appears that only one piece is flipping:
<transition name="flip" mode="out-in">
<slot v-if="!isShowing"></slot>
<img v-else src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/cartoonvideo14.jpeg" />
</transition>
See the Pen Vue in-out modes by Sarah Drasner (@sdras) on CodePen.
If we were to take out that mode, you can see that one flip obscures the other, and the animation looks jarring, not at all the effect we want to achieve:
See the Pen Vue in-out modes – no modes contrast by Sarah Drasner (@sdras) on CodePen.
JS Animation
We have some nice JS hooks that are very easy to use or not use as we see fit for our animation. All hooks pass in the el
parameter (short for element) except on the actual animation hooks, enter and leave, which also pass done
as a parameter, which, you guessed it, is used to tell Vue that the animation is completed. You’ll notice we’re also binding CSS to a falsy value to let the component know we’ll be using JavaScript instead of CSS.
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-Leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
:css="false">
</transition>
At the most basic level, this is really what you would need for an entrance and exit animation, including the corresponding methods:
<transition
@enter="enterEl"
@leave="leaveEl"
:css="false">
<!-- put element here-->
</transition>
methods: {
enterEl(el, done) {
//entrance animation
done();
},
leaveEl(el, done) {
//exit animation
done();
},
}
Here’s an example of how I would use this to plug into a GreenSock timeline:
new Vue({
el: '#app',
data() {
return {
message: 'This is a good place to type things.',
load: false
}
},
methods: {
beforeEnter(el) {
TweenMax.set(el, {
transformPerspective: 600,
perspective: 300,
transformStyle: "preserve-3d",
autoAlpha: 1
});
},
enter(el, done) {
...
tl.add("drop");
for (var i = 0; i < wordCount; i++) {
tl.from(split.words[i], 1.5, {
z: Math.floor(Math.random() * (1 + 150 - -150) + -150),
ease: Bounce.easeOut
}, "drop+=0." + (i/ 0.5));
...
}
}
});
See the Pen Vue Book Content Typer by Sarah Drasner (@sdras) on CodePen.
Two of the more interesting things to note in the above animation, I’m passing {onComplete:done}
as a parameter to the Timeline instance, and I’m using the beforeEnter
hook to place my TweenMax.set
code, which allows me to set any properties on the words I need for the animation before it happens, in this case, things like transform-style: preserve-3d
.
It’s important to note that you can also set what you want for the animation directly in the CSS as the default state as well. People sometimes ask me how to decide what to set in the CSS and what to set in TweenMax.set
. As a rule of thumb, I generally put any properties I need specifically for the animation into the TweenMax.set
. That way if something in the animation changes and I need to update it, it’s already part of my workflow.
Animations in Lifecycle Hooks
All of this is really nice, but what happens if you need to animate something very complex, something that works with a ton of DOM elements? This is a really nice time to use some lifecycle methods. In the following animation, we have used both the component as well as the
mounted()
method to create some animations.
See the Pen Vue Weather Notifier by Sarah Drasner (@sdras) on CodePen.
When we transition a single element, we’ll use the transition component, for instance, when the stroke around the phone button shows up:
<transition
@before-enter="beforeEnterStroke"
@enter="enterStroke"
:css="false"
appear>
<path class="main-button" d="M413,272.2c5.1,1.4,7.2,4.7,4.7,7.4s-8.7,3.8-13.8,2.5-7.2-4.7-4.7-7.4S407.9,270.9,413,272.2Z" transform="translate(0 58)" fill="none"/>
</transition>
beforeEnterStroke(el) {
el.style.strokeWidth = 0;
el.style.stroke = 'orange';
},
enterStroke(el, done) {
const tl = new TimelineMax({
onComplete: done
});
tl.to(el, 0.75, {
strokeWidth: 1,
ease: Circ.easeOut
}, 1);
tl.to(el, 4, {
strokeWidth: 0,
opacity: 0,
ease: Sine.easeOut
});
},
But when a component first shows up and we have 30 elements or more animating, it would not longer be efficient to wrap each one in a separate transition
component. So, we’ll use the lifecycle methods we mentioned in section 3 of this series to hook into the same event that the transition hook is using under the hook: mounted()
const Tornadoarea = {
template: '#tornadoarea',
mounted () {
let audio = new Audio('https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/tornado.mp3'),
tl = new TimelineMax();
audio.play();
tl.add("tornado");
//tornado timeline begins
tl.staggerFromTo(".tornado-group ellipse", 1, {
opacity: 0
}, {
opacity: 1,
ease: Sine.easeOut
}, 0.15, "tornado");
...
}
};
We can really use either depending on what’s more efficient and as you can see, you can create really complex effects. Vue offers a really beautiful and flexible API, not just for creating composable front-end architecture, but also for fluid movement and seamless transitions between views.
Conclusion
This series of articles is not intended to be documentation. Though we’ve covered a lot of ground, there’s still so much more to explore: routing, mixins, server-side rendering, etc. There are so many amazing things to work with. Head over to the very excellent docs and this repo full of examples and resources to dig in further. There is also a book called The Majesty of Vue.js, and courses on Egghead.io and Udemy.
Many thanks to Robin Rendle, Chris Coyier, Blake Newman, and Evan You for proofreading various sections of this series. I hope this series conveys why I’m so excited about Vue and helps you get up and running trying out some of the material!
Article Series:
- Rendering, Directives, and Events
- Components, Props, and Slots
- Vue-cli
- Vuex
- Animations (You are here!)
Intro to Vue.js: Animations is a post from CSS-Tricks