Creating Vue.js Transitions & Animations

My last two projects hurled me into the JAMstack. SPAs, headless content management, static generation... you name it. More importantly, they gave me the opportunity to learn Vue.js. More than "Build a To-Do App" Vue.js, I got to ship real-life, production-ready Vue apps.

The agency behind Snipcart (Spektrum) wanted to start using decoupled JavaScript frameworks for small to medium sites. Before using them on client projects, however, they chose to experiment on themselves. After a few of my peers had unfruitful experiences with React, I was given the green light to prototype a few apps in Vue. This prototyping morphed into full-blown Vue apps for Spektrum connected to a headless CMS. First, I spent time figuring out how to model and render our data appropriately. Then I dove head first into Vue transformations to apply a much-needed layer of polish on our two projects.

I've prepared live demos on CodePen and GitHub repos to go along with this article.

This post digs into Vue.js and the tools it offers with its transition system. It is assumed that you are already comfortable with the basics of Vue.js and CSS transitions. For the sake of brevity and clarity, we won't get into the "logic" used in the demo.

Handling Vue.js Transitions & Animations

Animations & transitions can bring your site to life and entice users to explore. Animations and transitions are an integral part of UX and UI design. They are, however, easy to get wrong. In complex situations like dealing with lists, they can be nearly impossible to reason about when relying on native JavaScript and CSS. Whenever I ask backend developers why they dislike front end so vehemently, their response is usually somewhere along the lines of "... animations".

Even for those of us who are drawn to the field by an urge to create intricate micro-interactions and smooth page transitions, it's not easy work. We often need to rely on CSS for performance reasons, even while working in a mostly JavaScript environment, and that break in the environment can be difficult to manage.

This is where frameworks like Vue.js step in, taking the guess-work and clumsy chains of setTimeout functions out of transitions.

The Difference Between Transitions and Animations

The terms transition and animation are often used interchangeably but are actually different things.

  • A transition is a change in the style properties on an element to be transitioned in a single step. They are often handled purely through CSS.
  • An animation is more complex. They are usually multi-step and sometimes run continuously. Animations will often call on JavaScript to pick up where CSS' lack of logic drops off.

It can be confusing, as adding a class could be the trigger for a transition or an animation. Still, it is an important distinction when stepping into the world of Vue because both have very different approaches and toolboxes.

Here's an example of transitions in use on Spektrum's site:

Using Transitions

The simplest way to achieve transition effects on your page is through Vue's <transition> component. It makes things so simple, it almost feels like cheating. Vue will detect if any CSS animations or transitions are being used and will automatically toggle classes on the transitioned content, allowing for a perfectly timed transition system and complete control.

First step is to identify our scope. We tell Vue to prepend the transition classes with modal, for example, by setting the component's name attribute. Then to trigger a transition all you need to do is toggle the content's visibility using the v-if or v-show attributes. Vue will add/remove the classes accordingly.

There are two "directions" for transitions: enter (for an element going from hidden to visible) and leave (for an element going from visble to hidden). Vue then provides 3 "hooks" that represent different timeframes in the transition:

  • .modal-enter-active / .modal-leave-active: These will be present throughout the entire transition and should be used to apply your CSS transition declaration. You can also declare styles that need to be applied from beginning to end.
  • .modal-enter / .modal-leave: Use these classes to define how your element looks before it starts the transition.
  • .modal-enter-to / .modal-leave-to: You've probably already guessed, these determine the styles you wish to transition towards, the "complete" state.

To visualize the whole process, take a look at this chart from Vue's documentation:

How does this translate into code? Say we simply want to fade in and out, putting the pieces together would look like this:

<button class="modal__open" @click="modal = true">Help</button>

<transition name="modal">
  <section v-if="modal" class="modal">
    <button class="modal__close" @click="modal = false">&times;</button>
  </section>
</transition>
.modal-enter-active,
.modal-leave-active { transition: opacity 350ms }

.modal-enter,
.modal-leave-to { opacity: 0 }

.modal-leave,
.modal-enter-to { opacity: 1 }

This is likely the most basic implementation you will come across. Keep in mind that this transition system can also handle content changes. For example, you could react to a change in Vue's dynamic <component>.

<transition name="slide">
  <component :is="selectedView" :key="selectedView"/>
</transition>
.slide-enter { transform: translateX(100%) }
.slide-enter-to { transform: translateX(0) }
.slide-enter-active { position: absolute }

.slide-leave { transform: translateX(0) }
.slide-leave-to { transform: translateX(-100%) }

.slide-enter-active,
.slide-leave-active { transition: all 750ms ease-in-out }

Whenever the selectedView changes, the old component will slide out to the left and the new one will enter from the right!

Here's a demo that uses these concepts:

See the Pen VueJS transition & transition-group demo by Nicolas Udy (@udyux) on CodePen.

Transitions on Lists

Things get interesting when we start dealing with lists. Be it some bullet points or a grid of blog posts, Vue gives you the <transition-group> component.

It is worth noting that while the <transition> component doesn't actually render an element, <transition-group> does. The default behaviour is to use a <span> but you can override this by setting the tag attribute on the <transition-group>.

The other gotcha is that all list items need to have a unique key attribute. Vue can then keep track of each item individually and optimize its performance. In our demo, we're looping over the list of companies, each of which has a unique ID. So we can set up our list like so:

<transition-group name="company" tag="ul" class="content__list">
  <li class="company" v-for="company in list" :key="company.id">
    <!-- ... -->
  </li>
</transition-group>

The most impressive feature of transition-group is how Vue handles changes in the list's order so seamlessly. For this, an additional transition class is available, .company-move (much like the active classes for entering and leaving), which will be applied to list items that are moving about but will remain visible.

In the demo, I broke it down a bit more to show how to leverage different states to get a cleaner end result. Here's a simplified and uncluttered version of the styles:

/* base */
.company {
  backface-visibility: hidden;
  z-index: 1;
}

/* moving */
.company-move {
  transition: all 600ms ease-in-out 50ms;
}

/* appearing */
.company-enter-active {
  transition: all 300ms ease-out;
}

/* disappearing */
.company-leave-active {
  transition: all 200ms ease-in;
  position: absolute;
  z-index: 0;
}

/* appear at / disappear to */
.company-enter,
.company-leave-to {
  opacity: 0;
}

Using backface-visibility: hidden on an element, even in the absence of 3D transforms, will ensure silky 60fps transitions and avoid fuzzy text rendering during transformations by tricking the browser into leveraging hardware acceleration.

In the above snippet, I've set the base style to z-index: 1. This assures that elements staying on page will always appear above elements that are leaving. I also apply a absolute positioning to items that are leaving to remove them from the natural flow, triggering the move transition on the rest of the items.

That's all we need! The result is, frankly, almost magic.

Using Animations

The possibilities and approaches for animation in Vue are virtually endless, so I've chosen one of my favourite techniques to showcase how you could animate your data.

We're going to use GSAP's TweenLite library to apply easing functions to our state's changes and let Vue's lightning fast reactivity reflect this on the DOM. Vue is just as comfortable working with inline SVG as it is with HTML.

We'll be creating a line graph with 5 points, evenly spaced along the X-axis, whose Y-axis will represent a percentage. You can take a look here at the result.

See the Pen SVG path animation with VueJS & TweenLite by Nicolas Udy (@udyux) on CodePen.

Let's get started with our component's logic.

new Vue({
  el: '#app',
  // this is the data-set that will be animated
  data() {
    return {
      points: { a: -1, b: -1, c: -1, d: -1, e: -1 }
    }
  },

  // this computed property builds an array of coordinates that
  // can be used as is in our path
  computed: {
    path() {
      return Object.keys(this.points)
        // we need to filter the array to remove any
        // properties TweenLite has added
        .filter(key => ~'abcde'.indexOf(key))
        // calculate X coordinate for 5 points evenly spread
        // then reverse the data-point, a higher % should
        // move up but Y coordinates increase downwards
        .map((key, i) => [i * 100, 100 - this.points[key]])
    }
  },

  methods: {
    // our randomly generated destination values
    // could be replaced by an array.unshift process
    setPoint(key) {
      let duration = this.random(3, 5)
      let destination = this.random(0, 100)
      this.animatePoint({ key, duration, destination })
    },
    // start the tween on this given object key and call setPoint
    // once complete to start over again, passing back the key
    animatePoint({ key, duration, destination }) {
      TweenLite.to(this.points, duration, {
        [key]: destination,
        ease: Sine.easeInOut,
        onComplete: this.setPoint,
        onCompleteParams: [key]
      })
    },
    random(min, max) {
      return ((Math.random() * (max - min)) + min).toFixed(2)
    }
  },

  // finally, trigger the whole process when ready
  mounted() {
    Object.keys(this.points).forEach(key => {
      this.setPoint(key)
    })
  }
});

Now for the template.

<main id="app" class="chart">
  <figure class="chart__content">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="-20 -25 440 125">
      <path class="chart__path" :d="`M${path}`"
        fill="none" stroke="rgba(255, 255, 255, 0.3)"
        stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>

      <text v-for="([ x, y ]) in path" :x="x - 10" :y="y - 7.5"
        font-size="10" font-weight="200" fill="currentColor">
        {{ 100 - (y | 0) + '%' }}
      </text>
    </svg>
  </figure>
</main>

Notice how we bind our path computed property to the path element's d attribute. We do something similar with the text nodes that output the current value for that point. When TweenLite updates the data, Vue reacts instantly and keeps the DOM in sync.

That's really all there is to it! Of course, additional styles were applied to make things pretty, which at this point you might realize is more work then the animation itself!

Live demos (CodePen) & GitHub repo

Go ahead, browse the live demos or analyze/re-use the code in our open source repo!

Conclusion

I've always been a fan of animations and transitions on the web, but I'm also a stickler for performance. As a result, I'm always very cautious when it comes to relying on JavaScript. However, combining Vue's blazing fast and low-cost reactivity with its ability to manage pure CSS transitions, you would really have to go overboard to have performance issues.

It's impressive that such a powerful framework can offer such a simple yet manageable API. The animation demo, including the styling, was built in only 45 minutes. And if you discount the time it took to set up the mock data used in the list-transition, it's achievable in under 2 hours. I don't even want to imagine the migraine-inducing process of building similar setups without Vue, much less how much time it would take!

Now get out there and get creative! The use cases go far beyond what we have seen in this post: the only true limitation is your imagination. Don't forget to check out the transitions and animations section in Vue.js' documentation for more information and inspiration.


This post originally appeared on Snipcart's blog. Got comments, questions? Add them below!