Vue.js 3 Animations & Transitions Tutorial

In this Vue tutorial we learn how to animate elements in a component with transitions.

We cover the transition component, animating states, custom keyframe animations, conditional animation and transition modes.

Lesson Video

If you prefer to learn visually, you can watch this lesson in video format.

Lesson Project

If you want to follow along with the examples, you will need to create an app generated by the Vue CLI .

Our root App component will have a simple paragraph that can be toggled on and off with a button. We’ll also add some styling to the paragraph so that it will be easier to see the animation later on.

Example: src/App.vue
<template>
  <button @click="showP = !showP">Toggle</button>
  <p v-if="showP">Hello there</p>
</template>

<script>
export default {
  data() {
    return { showP: false }
  }
}
</script>

<style>
p,button{display:grid;margin:20px auto}
p{text-align:center;padding:20px 0;
  background:gainsboro;width:200px}
</style>

Animation in Vue

Just like regular websites, we can use CSS animations and transitions to help create a better user experience on our app.

Using transitions and animations gives our app several benefits.

  • It helps the user navigate the app by understanding connections between elements.
  • It let’s the user know what’s happening by giving feedback in response to an action.
  • It can draw attention to new or essential features and elements.
  • It can encourage user engagement.
  • It creates a unique and memorable user experience.
  • etc.

To help with our animations and transitions, Vue provides us with the following two components.

How to animate an element with the transition component

The transition component wraps the element we want to animate with open-and-close transition tags.

Example: transition
<transition>
  <element />
</transition>

Vue will add transition classes to the element that controls the animation of the element onto the page.

  • v-enter-from is applied before the element enters the browser window. This is where we set the element’s starting CSS state.
  • v-enter-to is applied when the element enters the browser window. This is where we set the element’s ending CSS state.
  • v-enter-active is applied when the element is transitioning from one state to another. This is where we set the duration and easing of the transition.
Example: entering state
/* start invisible */
enter-from {
  opacity: 0
}
/* end opaque */
enter-to {
  opacity: 1
}
/* transition opacity over .3      */
/* seconds with an 'ease' function */
enter-active {
  transition: opacity .3s ease
}

To demonstrate, let’s wrap the paragraph in our example in transition tags and add transitioning from invisible to opaque.

Example: src/App.vue
<template>
  <button @click="showP = !showP">Toggle</button>
  <transition>
    <p v-if="showP">Hello there</p>
  </transition>
</template>

<script>
export default {
  data() {
    return { showP: false }
  }
}
</script>

<style>
.v-enter-from {
  opacity: 0
}
.v-enter-to {
  opacity: 1
}
.v-enter-active {
  transition: opacity 2s ease
}

p,button{display:grid;margin:20px auto}
p{text-align:center;padding:20px 0;
  background:gainsboro;width:200px}
</style>

If we run the example in the browser and click on the button, the paragraph becomes more and more visible over 2 seconds.

We also have three classes for when the element leaves the page. They work exactly the same as the entering classes.

  • v-leave-from
  • v-leave-to
  • v-leave-active

To demonstrate, we’ll let the paragraph transition out from opaque to invisible. Because we want the duration and easing to be the same, we can combine the two active class selectors by separating them with a comma.

Example: src/App.vue
<template>
  <button @click="showP = !showP">Toggle</button>
  <transition>
    <p v-if="showP">Hello there</p>
  </transition>
</template>

<script>
export default {
  data() {
    return { showP: false }
  }
}
</script>

<style>
.v-enter-from { opacity: 0 }
.v-enter-to   { opacity: 1 }

.v-enter-active,
.v-leave-active {
  transition: opacity 2s ease
}
.v-leave-from { opacity: 1 }
.v-leave-to   { opacity: 0 }

p,button{display:grid;margin:20px auto}
p{text-align:center;padding:20px 0;
  background:gainsboro;width:200px}
</style>

If we run the example in the browser and toggle the paragraph, it will fade in and then out.

Default element states

If we’re transitioning to a state that’s the default for an element, we don’t have to create a transition for it.

For example, the default opacity for any element is 1, so we don’t have to specify it in our example’s v-enter-to class. The same goes for the v-leave-from class.

And because the classes are now empty, we can remove them completely.

Example: src/App.vue
<template>
  <button @click="showP = !showP">Toggle</button>
  <transition>
    <p v-if="showP">Hello there</p>
  </transition>
</template>

<script>
export default {
  data() {
    return { showP: false }
  }
}
</script>

<style>
.v-enter-from { opacity: 0 }

.v-enter-active,
.v-leave-active {
  transition: opacity 2s ease
}
.v-leave-to { opacity: 0 }

p,button{display:grid;margin:20px auto}
p{text-align:center;padding:20px 0;
  background:gainsboro;width:200px}
</style>

If we run the example above and toggle the paragraph, everything still works as expected.

Named transitions

Vue allows us to name our transitions by using the name attribute on the transition component.

When we name a transition, Vue automatically replaces the v in the class name, with our custom name.

Syntax: named transition
<transition name="transition-name">
  <element />
</transition>

<style>
  .transition-name-enter-from {}
  /* etc. */
</style>

To demonstrate, let’s add a name to our example’s transition and replace the v in the class names with the new name.

Example: src/App.vue
<template>
  <button @click="showP = !showP">Toggle</button>
  <transition name="fade">
    <p v-if="showP">Hello there</p>
  </transition>
</template>

<script>
export default {
  data() {
    return { showP: false }
  }
}
</script>

<style>
.fade-enter-from { opacity: 0 }

.fade-enter-active,
.fade-leave-active {
  transition: opacity 2s ease
}
.fade-leave-to   { opacity: 0 }

p,button{display:grid;margin:20px auto}
p{text-align:center;padding:20px 0;
  background:gainsboro;width:200px}
</style>

If we run the example in the browser, everything still works as expected.

How to apply a transition on an element's initial render

We can tell Vue that we want the animation class to apply when an element is rendered on the page. We do this by adding the appear attribute to the transition component.

tip We don’t have to explicitly set the appear attribute to true. It’s existence implies a true value.

To demonstrate, let’s add a transition component to our example’s button with a named transition and the appear attribute. We’ll let the button fade in while sliding in from the top of the page.

We’ll also remove the paragraph’s transition to keep the example small.

Example: src/App.vue
<template>
  <transition name="slide-down-fade" appear>
    <button @click="showP = !showP">Toggle</button>
  </transition>

  <p v-if="showP">Hello there</p>
</template>

<script>
export default {
  data() {
    return { showP: false }
  }
}
</script>

<style>
.slide-down-fade-enter-from {
  opacity: 0;
  transform: translateY(-30px)
}
.slide-down-fade-enter-active {
  transition: all 2s ease
}

p,button{display:grid;margin:20px auto}
p{text-align:center;padding:20px 0;
  background:gainsboro;width:200px}
</style>

If we run the example in the browser, the button will fade and slide in when we load the page. If we remove the appear attribute, the animation won’t happen.

How to animate an element with the custom keyframe animation

When we need to create more complex animations, we have to use keyframe animations.

A keyframe animation performs animations at certain stages of its lifecycle, specified as percentages. If we want a keyframe animation to be applied to the Vue transition, we add it to the enter or leave active class.

Example: keyframe animation
@keyframes name {
  0% { /* do something*/ }
  50% { /* do something*/ }
  100% { /* do something*/ }
}

.v-enter-active,
.v-leave-active {
  animation: name 1s ease
}

To demonstrate, let’s change our example to use a keyframe animation that adds a shake to the fade and slide in.

Example: src/App.vue
<template>
  <transition name="shake" appear>
    <button @click="showP = !showP">Toggle</button>
  </transition>

  <p v-if="showP">Hello there</p>
</template>

<script>
export default {
  data() {
    return { showP: false }
  }
}
</script>

<style>
.shake-enter-active {
  animation: shake 1s ease
}

@keyframes shake {
  /* fade in and slide down */
  0% {
    opacity: 0;
    transform: translateY(-30px)
  }
  50% {
    opacity: 1;
    transform: translateY(0px)
  }
  /* shake from right to left */
  60% { transform: translateX( 5px) }
  65% { transform: translateX(-5px) }
  70% { transform: translateX( 4px) }
  75% { transform: translateX(-4px) }
  80% { transform: translateX( 3px) }
  85% { transform: translateX(-3px) }
  90% { transform: translateX( 2px) }
  95% { transform: translateX(-2px) }
  100%{ transform: translateX( 0px) }
}

p,button{display:grid;margin:20px auto}
p{text-align:center;padding:20px 0;
  background:gainsboro;width:200px}
</style>

If we run the example in the browser, the button will shake a little after the fade and slide in.

How to animate multiple elements with the transition-group component

If we want to animate multiple elements, we need to use the transition-group component. This component replaces the original container element.

For example, if we have an unordered list, we would replace the ul tags with the transition-group tags.

Syntax: transition-group
<transition-group>
  <li>List Item</li>
</transition-group>

<!-- instead of -->
<ul>
  <li>List Item</li>
</ul>

By default, the transition-group tag will not render a wrapper when the application is compiled, but we can specify one with the tag attribute.

Syntax: custom tag
<transition-group tag="ul">
  <li>List Item</li>
</transition-group>

The transition-group component also supports names and the appear attribute.

Syntax: named group and appear
<transition-group tag="ul" name="transition-name" appear>
  <li>List Item</li>
</transition-group>

As an example, we’ll create a simple Todo app. To keep the demonstration simple, it will start off with 2 predefined items and will only be able to add new items, not delete them.

Example: src/App.vue
<template>
  <input type="text" v-model="newTodo" placeholder="New todo">
  <button @click="addTodo">Add todo</button>

  <ul>
    <li v-for="todo in todos" :key="todo.id">{{ todo.content }}</li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      newTodo: '',
      todos: [
        { id: 1, content: 'Walk dog' },
        { id: 2, content: 'Buy milk' }
      ]
    }
  },
  methods: {
    addTodo() {
      this.todos.push({
        id: this.UID(),
        content: this.newTodo
      })
      this.newTodo = ''
    },
    UID() {
      return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
        (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
      )
    }
  }
}
</script>

If we run the example and add a new todo item, it will immediately show in the list.

Now, let’s change the example and replace the ul in the template with the transition-group tag. We’ll also specify the tag we want to render as “ul” and the animation name as “slide”.

Because the final animated state is the default element state of the list, we won’t specify the v-enter-to class.

Example: src/App.vue
<template>
  <input type="text" v-model="newTodo" placeholder="New todo">
  <button @click="addTodo">Add todo</button>

  <transition-group tag="ul" name="slide">
    <li v-for="todo in todos" :key="todo.id">{{ todo.content }}</li>
  </transition-group>
</template>

<script>
export default {
  data() {
    return {
      newTodo: '',
      todos: [
        { id: 1, content: 'Walk dog' },
        { id: 2, content: 'Buy milk' }
      ]
    }
  },
  methods: {
    addTodo() {
      this.todos.push({
        id: this.UID(),
        content: this.newTodo
      })
      this.newTodo = ''
    },
    UID() {
      return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
        (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
      )
    }
  }
}
</script>

<style>
.slide-enter-from {
  opacity: 0;
  color: limegreen;
  transform: translateY(30px)
}
.slide-enter-active {
  transition: opacity .8s ease,
              transform .8s ease,
              color 5s ease
}
</style>

This time when we add a new todo item in the browser, it will slide in from the bottom and turn from green to bla

Move transitions

Vue allows us to control a changes in element positions with the v-move class.

All we need to do is add the v-move class and specify the transition we want it to affect. If the transition-group is named, we use that name in the class.

Syntax: v-move
/* unnamed */
.v-move {
  transition: effect 1s ease
}

/* named */
.transition-name-move {
  transition: effect 1s ease
}

As an example, let’s create an app that shows a sortable list of characters from popular games. We’ll let the list slide in from the bottom when the component first renders with the appear attribute.

Example: src/App.vue
<template>
  <button @click="sortCharacters">Sort (A - Z)</button>

  <transition-group tag="ul" name="slide" appear>
    <li v-for="character in characters" :key="character">
      {{ character }}
    </li>
  </transition-group>
</template>

<script>
export default {
  data() {
    return {
      characters: [
        'Vaas Montenegro', 'Joel Miller', 'Geralt of Rivia',
        'Gordon Freeman', 'Sam Fisher', 'Sarah Kerrigan',
        'Marcus Fenix', 'John Marston', 'Jim Raynor',
        'Adam Jensen', 'Ezio Auditore', 'Nathan Drake'
      ]
    }
  },
  methods: {
    sortCharacters() {
      this.characters.sort()
    }
  }
}
</script>

<style>
.slide-enter-from {
  opacity: 0;
  transform: translateY(160px)
}
.slide-enter-active {
  transition: transform 1.4s ease,
              opacity 1.8s ease
}
</style>

If we run the example in the browser and click on the “Sort” button, the list is sorted alphabetically, but there’s no animation when it does.

So let’s add the -move class and target the transform from the -enter-from class.

Example: src/App.vue
<template>
  <button @click="sortCharacters">Sort (A - Z)</button>

  <transition-group tag="ul" name="slide" appear>
    <li v-for="character in characters" :key="character">
      {{ character }}
    </li>
  </transition-group>
</template>

<script>
export default {
  data() {
    return {
      characters: [
        'Vaas Montenegro', 'Joel Miller', 'Geralt of Rivia',
        'Gordon Freeman', 'Sam Fisher', 'Sarah Kerrigan',
        'Marcus Fenix', 'John Marston', 'Jim Raynor',
        'Adam Jensen', 'Ezio Auditore', 'Nathan Drake'
      ]
    }
  },
  methods: {
    sortCharacters() {
      this.characters.sort()
    }
  }
}
</script>

<style>
.slide-enter-from {
  opacity: 0;
  transform: translateY(160px)
}
.slide-enter-active {
  transition: transform 1.4s ease,
              opacity 1.8s ease
}
/* target transform only */
.slide-move {
  transition: transform .4s
}
</style>

This time when we sort the list in the browser, it’s animated.

How to animate conditional elements

When elements are rendered based on a condition, we use the transition component and not the the transition-group component.

Syntax: conditional elements
<transition>
  <element v-if="">Content</element>
  <element v-else>Content</element>
</transition>

Even though there are multiple elements, they won’t be rendered together. One element replaces the other based on the condition.

To demonstrate, let’s create an example that toggles between an error and success message if a data property is true or false.

Example: src/App.vue
<template>
  <transition name="switch">
    <p class="e" v-if="error">Error</p>
    <p class="s" v-else>Success</p>
  </transition>

  <button @click="error = !error">Toggle Message</button>
</template>

<script>
export default {
  data() {
    return { error: false }
  }
}
</script>


<style>
.switch-enter-from,
.switch-leave-to {
  opacity: 0;
  transform: translateY(-20px)
}
.switch-enter-active,
.switch-leave-active {
  transition: all .2s ease-out
}

#app {text-align:center}
p {width:200px;margin:20px auto;padding:10px;border:1px solid gray}
.e {color:#cc0f35;background:#feecf0;border-color:#f14668}
.s {color:#257953;background:#effaf5;border-color:#48c78e}
</style>

When we run the example in the browser and toggle between the messages, they animate successfully.

The animations look a bit jerky as they switch, but that’s not because we’re using transition instead of transition-group .

How to fix jerky animations with transition modes

When we’re switching out elements (or components), the new one will try to render on the page while the old one is still exiting, causing a sort of snapping effect.

To fix this, Vue gives us the mode attribute with two settings.

  1. in-out will transition the new element in first, then transition the old element out.
  2. out-in will transition the old element out first, then transition the new element in.
Syntax: mode
<transition mode="out-in">
  <element v-if="">Content</element>
  <element v-else>Content</element>
</transition>

To demonstrate, let’s add the out-in mode to our example.

Example: src/App.vue
<template>
  <transition name="switch" mode="out-in">
    <p class="e" v-if="error">Error</p>
    <p class="s" v-else>Success</p>
  </transition>

  <button @click="error = !error">Toggle Message</button>
</template>

<script>
export default {
  data() {
    return { error: false }
  }
}
</script>


<style>
.switch-enter-from,
.switch-leave-to {
  opacity: 0;
  transform: translateY(-20px)
}
.switch-enter-active,
.switch-leave-active {
  transition: all .2s ease-out
}

#app {text-align:center}
p {width:200px;margin:20px auto;padding:10px;border:1px solid gray}
.e {color:#cc0f35;background:#feecf0;border-color:#f14668}
.s {color:#257953;background:#effaf5;border-color:#48c78e}
</style>

This time when we toggle between the messages, the animation works correctly and there’s no more snapping.