Image

animating-vue

9 min read
Last update: December 19, 2021

Intro to GSAP 3

In this lesson, we’ll learn how to combine Vue with a very powerful and popular animation library called GreenSock Animation Platform (GSAP), which at the time of this writing just released its new and improved version 3. We’ll explore a couple different ways of using GSAP to animate our Vue interfaces.


What’s GSAP?

If you look at what leading developers in the web animation community are using, you’ll find that GSAP is a go-to tool for them. The library is a robust, high-performance library that enables you to animate anything that JavaScript can touch, and it smooths out browser inconsistencies by default. Previously, the library was broken into sections (TweenLite, TweenMax, TimelineLite, TimelineMax) but now, with GSAP 3, everything is self-contained within one singular GSAP object.

So how does it work? You essentially give GSAP a target to animate, whether that’s an element, component, or some data, then you tell GSAP how to animate that target by passing it an object full of instructions, including the duration of the animation and an optional easing function. But let’s stop talking about it and started using it!


Our First GSAP Animation

To get started using GSAP, we’ll look at a simple example, where our animation code is contained within methods that are triggered at different points within the transition component’s lifecycle; before it enters the DOM (before-enter) and once it’s entered (enter).

Here’s our starting code:

📃 src/views/Simple.vue

<template>
    <transition appear @before-enter="beforeEnter" @enter="enter" :css="false">
        <div class="card"></div>
    </transition>
</template>

<script>
    export default {
        methods: {
            beforeEnter(el) {
                // starting style
            },
            enter(el, done) {
                // style to transition to once entered
            }
        }
    }
</script>

<style scoped>
    .card {
        display: block;
        margin: 0 auto 0 auto;
        height: 6.5em;
        width: 6.5em;
        border-radius: 1%;
        background-color: #16c0b0;
        box-shadow: 0.08em 0.03em 0.4em #ababab;
    }
</style>

As you can see, we have a transition component that wraps a card div, which will appear when this component is initially loaded. We’re using the transition component’s JavaScript Hooks before-enter and enter to trigger our methods by the same names, and binding false to :css so Vue won’t worry about adding/removing any transition classes. As a reminder, we covered the transition-based JavaScript Hooks in the previous lesson.

Now let’s import GSAP, which I’ve already installed into the demo project. You could install it yourself by running npm install gsap from the terminal.

📃 src/views/Simple.vue

<script>
    import gsap from 'gsap'
    export default {
        ...
    }
</script>

Now that GSAP is imported, we can move on to the methods that will be triggered by the transition component’s JavaScript Hooks. First, we’ll need to define how we want the card to be styled before it enters our interface. We’ll set that up in our beforeEnter method.

📃 src/views/Simple.vue

methods: {
beforeEnter(el) {
el.style.opacity = 0
el.style.transform = 'scale(0,0)'
},

We’re telling it to start out invisible and to have a scale of 0 for both its x and y values. Now, in our enter method, we’ll use GSAP to animate it into a visible card at full scale.

📃 src/views/Simple.vue

enter(el, done) {
gsap.to(el, {
duration: 1,
opacity: 1,
scale: 1,
onComplete: done
})
}

So what’s going on here? We’re using the gsap.to() method, and passing it some arguments. The to() method is used to define what we want the element to animate to, versus start from.

So what do we want the card's styles to transition to? We set that up in this method’s arguments. The first argument (el) tells GSAP which element to animate. Next, we’re passing in an object that gives GSAP the styles for this element to end up with, or to animate to. As you can see, we’re making the card visible (opacity: 1) and fully scaled to 100% with scale: 1.

GSAP also has a property where we can pass in a method to run when the animate is complete. So we’re passing in done to run when this transition is complete (onComplete). This lets Vue know that this step in the transition component’s lifecycle is complete, so Vue can move on to the next hook.

If we run this in the browser, we’ll see that when the card appears, it fades from invisible to visible while its scale increases from 0 to 100%. Great.

This looks good so far, but what if we want our card to bounce in as in enters? We can achieve that effect by using a bounce ease, like so:

📃 src/views/Simple.vue

enter(el, done) {
gsap.to(el, {
duration: 1,
opacity: 1,
scale: 1,
ease: 'bounce.out',
onComplete: done
})
}

Great, now the card bounces into place. Of course there are plenty more GSAP easing options you can check out to fine-tune your animations. Now that we’ve built this simple GSAP animation, let’s move on to a bit more complex one.


Staggering Elements

It’s fairly common to see elements that stagger into place when they enter a web interface. You can imagine doing an API call for a list of data that you’re displaying within cards, and each card staggers into place. We can accomplish this fairly easily using GSAP.

It’s worth noting before we start coding this that we don’t always need to rely on using the transition / transition-group components and their JavaScript Hooks when we’re building out third-party-based JavaScript animations like this. There are many ways to skin this cat, and using the Vue component’s lifecycle methods is one of them. So in this example, we’ll rely on our component’s mounted hook. Let’s take a look at the starting code.

📃 src/views/Stagger.vue

<template>
    <div id="container">
        <div v-for="card in cards" :key="card.id" class="card"></div>
    </div>
</template>

<script>
    import gsap from 'gsap'

    export default {
        data() {
            return {
                cards: [{
                        id: 1298
                    },
                    {
                        id: 8748
                    },
                    {
                        id: 4919
                    },
                    {
                        id: 5527
                    },
                    {
                        id: 9428
                    },
                    {
                        id: 7103
                    }
                ]
            }
        },
        mounted() {
            // stagger cards into position
        }
    }
</script>

<style scoped>
    #container {
        display: flex;
        flex-direction: row;
        flex-wrap: wrap;
        justify-content: space-evenly;
    }

    .card {
        height: 6.5em;
        width: 6.5em;
        border-radius: 1%;
        background-color: #16c0b0;
        box-shadow: 0.08em 0.03em 0.4em #ababab;
        padding-top: 1em;
        margin-top: 0.5em;
        margin-right: 0.5em;
    }
</style>

As you can see, we’re creating a list of cards based on our data, using v-for. If we were to view this component in the browser, the cards would already be in their position. But in this example, we want them to stagger into place when the component is mounted, so let’s set that up.

Because we want our cards to stagger into place, we are using GSAP’s from() method instead of to(). If this doesn’t make sense at first, look at it this way: we want to provide GSAP with information about the starting state for these cards, then GSAP will move them from that starting state into their final position within the interface, staggering them in as they enter.

The full stagger code looks like this:

📃 src/views/Stagger.vue

mounted() {
gsap.from('.card', {
duration: 0.5,
opacity: 0,
scale: 0,
y: 200,
ease: 'power1',
stagger: 0.1
})
}

Above you can see that we are telling GSAP which element to animate (every.card), then passing in the animation instructions. Those instructions inform GSAP to conduct this animation over the course of a 0.5second duration, and to start the cards out as invisible (opacity: 0), at a scale of 0, and to position the cards 200 pixels down in the y direction before they stagger in. We’ve also given this animation an ease with the string name of 'power1'.

Finally, the stagger property is the secret sauce for our stagger effect. We’ve given it a value of 0.1. This is the delay, or the amount of time to wait, before staggering in the next card.

Now, if we check this out in the browser, we’ll see they’re staggering into place just like we wanted them to.


A more nuanced Stagger

That was a pretty basic stagger animation, but by adding onto the stagger property, we can create some more nuanced behavior. For example, our stagger could look like this:

📃 src/views/Stagger.vue

mounted() {
gsap.from('.card', {
duration: 0.5,
opacity: 0,
scale: 0,
y: 200,
ease: 'power1',
stagger: {
from: 'edges',
each: 0.1
}
})
}

Now our stagger property is an object that includes the each property, which is used for the delay. You can think of it as the amount of time to wait between “each” element’s entrance. There is also now the from property, which enables us to tell GSAP where within the collection of elements to start the stagger from. With 'edges' we’re telling it to start from the outside elements and work its way inward. If we said from: 'center' it would start from the center and work its way out to the edges.

To learn more about advanced staggers, visit here.


Let’s ReVue

We’ve learned how to use the industry standard JavaScript-based GSAP animation library to build out animations that happen in two different ways: when JavaScript Hooks are triggered on the transition component, as well as when our component’s lifecycle methods fire. In the next lesson, we’ll continue using GSAP and look at animating based upon some always-changing state.

Simple.vue

<template>
    <transition appear @before-enter="beforeEnter" @enter="enter" :css="false">
        <div class="card"></div>
    </transition>
</template>

<script>
    import gsap from 'gsap'
    export default {
        methods: {
            beforeEnter(el) {
                el.style.opacity = 0
                el.style.transform = 'scale(0,0)'
            },
            enter(el, done) {
                gsap.to(el, {
                    duration: 1,
                    opacity: 1,
                    scale: 1,
                    ease: 'bounce.out',
                    onComplete: done
                })
            }
        }
    }
</script>

<style scoped>
    .card {
        display: block;
        margin: 0 auto 0 auto;
        height: 6.5em;
        width: 6.5em;
        border-radius: 1%;
        background-color: #16c0b0;
        box-shadow: 0.08em 0.03em 0.4em #ababab;
    }
</style>

Stagger.vue

<template>
    <div id="container">
        <div v-for="card in cards" :key="card.id" class="card"></div>
    </div>
</template>

<script>
    import gsap from 'gsap'
    export default {
        data() {
            return {
                cards: [{
                        id: 1298
                    },
                    {
                        id: 8748
                    },
                    {
                        id: 4919
                    },
                    {
                        id: 5527
                    },
                    {
                        id: 9428
                    },
                    {
                        id: 7103
                    }
                ]
            }
        },
        mounted() {
            gsap.from('.card', {
                duration: 0.5,
                opacity: 0,
                scale: 0,
                y: 200,
                ease: 'power1',
                stagger: {
                    from: 'edges',
                    each: 0.1
                }
            })
        }
    }
</script>

<style scoped>
    #container {
        display: flex;
        flex-direction: row;
        flex-wrap: wrap;
        justify-content: space-evenly;
    }

    .card {
        height: 6.5em;
        width: 6.5em;
        border-radius: 1%;
        background-color: #16c0b0;
        box-shadow: 0.08em 0.03em 0.4em #ababab;
        padding-top: 1em;
        margin-top: 0.5em;
        margin-right: 0.5em;
    }
</style>