Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 057 / 100

Transitions and animations in Vue: a step-by-step guide

Motion built into the framework.

Expona Admin9 min read

Transitions and animations in Vue: a step-by-step guide

Motion is one of those details that separates a polished application from one that feels stiff and disconnected. Vue's built-in transition system lets you add entrance and exit animations, animate list changes, and respect user preferences—all without reaching for external libraries. In this step-by-step guide, we'll walk through the core transition tools available in Vue and show you how to use them in real production scenarios.

Why transitions matter in production

When elements appear and disappear without animation, users notice jarring layout shifts. A modal that fades in instead of popping into place feels intentional. A deleted item that slides out of a list communicates what just happened. These micro-interactions build confidence in the interface and reduce cognitive friction.

Vue's transition system is framework-native: it manages enter and leave states, applies CSS classes at the right moments, and hooks into JavaScript lifecycle events. You control everything—duration, timing, easing—and Vue handles the timing orchestration for you.

The <Transition> component: animate entering and leaving elements

The most straightforward use case is showing and hiding a single element. Vue's <Transition> wrapper component detects when its child enters or leaves the DOM and applies CSS classes during the animation phases.

Here's a common pattern: a confirmation dialog that fades in when triggered and fades out when dismissed.

typescript
<template>
  <Transition>
    <div v-if="showDialog" class="modal-overlay">
      <div class="modal-content">
        <h3>Confirm action</h3>
        <p>Are you sure you want to proceed?</p>
        <button @click="confirm">Yes</button>
        <button @click="showDialog = false">Cancel</button>
      </div>
    </div>
  </Transition>
</template>

<script setup>
import { ref } from 'vue'

const showDialog = ref(false)

const confirm = () => {
  console.log('Action confirmed')
  showDialog.value = false
}
</script>

<style scoped>
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-content {
  background: white;
  padding: 2rem;
  border-radius: 8px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
</style>

By itself, this doesn't animate anything yet—it just renders. You need to add CSS classes that Vue applies during the transition phases. The <Transition> component requires a name prop to generate class names, or you can use the default v- prefix.

typescript
<Transition name="fade">
  <div v-if="showDialog" class="modal-overlay">
    <!-- content -->
  </div>
</Transition>

<style scoped>
/* Enter phase: element is added to DOM, animation begins */
.fade-enter-active {
  transition: opacity 0.3s ease;
}

/* Starting state for entering element */
.fade-enter-from {
  opacity: 0;
}

/* Ending state (default styles) */
.fade-enter-to {
  opacity: 1;
}

/* Leave phase: element is still in DOM, animation plays */
.fade-leave-active {
  transition: opacity 0.3s ease;
}

/* Starting state for leaving element */
.fade-leave-from {
  opacity: 1;
}

/* Ending state for leaving element */
.fade-leave-to {
  opacity: 0;
}
</style>

Vue applies these classes in sequence: enter-from for one frame, then enter-active, then enter-to when the animation completes. The same pattern applies to leave. This lets you define smooth transitions without JavaScript.

The <TransitionGroup> component: animate lists

Animating individual elements is useful, but lists present a trickier challenge. When an item is deleted from a list of tasks or products, you want that item to slide out while other items shift into place. <TransitionGroup> handles this by rendering a DOM element (default: <span>, but often a <div>) and animating both enter/leave and position changes.

Here's a realistic e-commerce scenario: a shopping cart where users can remove items and see smooth animations.

typescript
<template>
  <div class="cart">
    <h2>Shopping Cart</h2>
    <TransitionGroup name="list" tag="ul" class="cart-items">
      <li v-for="item in cartItems" :key="item.id" class="cart-item">
        <div class="item-info">
          <span>{{ item.name }}</span>
          <span class="price">${{ item.price }}</span>
        </div>
        <button @click="removeItem(item.id)">Remove</button>
      </li>
    </TransitionGroup>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const cartItems = ref([
  { id: 1, name: 'Widget A', price: 29.99 },
  { id: 2, name: 'Widget B', price: 49.99 },
  { id: 3, name: 'Widget C', price: 19.99 },
])

const removeItem = (id) => {
  const index = cartItems.value.findIndex(item => item.id === id)
  if (index > -1) {
    cartItems.value.splice(index, 1)
  }
}
</script>

<style scoped>
.cart-items {
  list-style: none;
  padding: 0;
  margin: 0;
}

.cart-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem;
  border-bottom: 1px solid #eee;
  background: white;
}

/* Enter animation */
.list-enter-active {
  transition: all 0.3s ease;
}

.list-enter-from {
  opacity: 0;
  transform: translateX(-30px);
}

/* Leave animation */
.list-leave-active {
  transition: all 0.3s ease;
}

.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

/* Move animation (reordering existing items) */
.list-move {
  transition: transform 0.3s ease;
}
</style>

The key addition here is .list-move, which animates items that shift position when a sibling is removed. Without this, other items would jump into place. The tag="ul" tells <TransitionGroup> to render as an unordered list instead of a span.

CSS classes: v-enter-from, v-leave-to and friends

Understanding the full lifecycle of transition CSS classes is essential for building predictable animations. Vue applies these in order during the enter phase:

  1. v-enter-from — Applied immediately when element is inserted. This is your starting state.
  2. v-enter-active — Applied during the entire enter animation. Attach your transition property here.
  3. v-enter-to — Applied at the moment the animation begins, representing the final state.

The leave phase mirrors this:

  1. v-leave-from — Applied immediately when element begins to leave. This is usually the same as your default styles.
  2. v-leave-active — Applied during the entire leave animation. Attach your transition property here.
  3. v-leave-to — Applied when the animation begins, representing the final state before removal.

When you name your transition (e.g., name="slide"), Vue prefixes these classes with your name: slide-enter-from, slide-leave-to, and so on.

A common pattern in production is combining multiple properties—opacity, transform, and sometimes filter—for richer motion:

typescript
<style scoped>
.toast-enter-active,
.toast-leave-active {
  transition: opacity 0.4s ease, transform 0.4s ease;
}

.toast-enter-from {
  opacity: 0;
  transform: translateY(-20px) scale(0.95);
}

.toast-leave-to {
  opacity: 0;
  transform: translateY(20px) scale(0.95);
}
</style>

This creates a toast notification that slides and fades in from above and slides and fades out below—a subtle but effective motion pattern.

Respect preferences: reduce motion when users ask

Not all users want animations. Some have vestibular disorders or motion sensitivity, and modern browsers expose this preference via the prefers-reduced-motion media query. Respecting this preference is both accessible and courteous.

Here's how to check and respond to this preference in Vue:

typescript
<script setup>
import { ref, computed, onMounted } from 'vue'

const prefersReducedMotion = ref(false)

onMounted(() => {
  const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
  prefersReducedMotion.value = mediaQuery.matches
  
  // Listen for changes (in case user adjusts settings)
  mediaQuery.addEventListener('change', (e) => {
    prefersReducedMotion.value = e.matches
  })
})

const transitionDuration = computed(() => {
  return prefersReducedMotion.value ? '0s' : '0.3s'
})
</script>

<template>
  <Transition :name="prefersReducedMotion ? 'instant' : 'fade'">
    <div v-if="showDialog">
      <!-- content -->
    </div>
  </Transition>
</template>

<style scoped>
/* Normal animations */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* No animations for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
  .fade-enter-active,
  .fade-leave-active {
    transition: opacity 0s;
  }
}

/* Instant alternative (no animation at all) */
.instant-enter-active,
.instant-leave-active {
  transition: none;
}
</style>

The most straightforward approach is using @media (prefers-reduced-motion: reduce) in your CSS to disable or shorten animations. You can also conditionally switch transition names in the template, but the media query approach is simpler and doesn't require JavaScript state management.

Common mistakes

Forgetting the transition property in -active classes. The CSS classes are just markers—Vue doesn't apply the transition automatically. You must add transition: property duration easing in your -enter-active or -leave-active classes.

Using the same CSS class for enter and leave with different final states. This causes incorrect animation. Always define both enter-from and leave-to if they differ from your default styles.

Animating properties that trigger layout recalculation. Properties like width, height, padding, and margin cause the browser to recalculate layout on every frame, hurting performance. Use transform and opacity instead—these animate on the GPU with minimal overhead.

Not providing a :key on list items. <TransitionGroup> needs stable keys to track which items enter, leave, and move. Without proper keys, animations will be unreliable or absent.

Ignoring prefers-reduced-motion. This isn't just a nice-to-have. WCAG guidelines recommend respecting motion preferences, and users will appreciate the consideration.

Key takeaways

  • Use <Transition> to animate single elements entering and leaving the DOM; use <TransitionGroup> for lists and complex layouts.
  • Vue's transition classes follow a predictable pattern: -enter-from, -enter-active, -enter-to, and mirrored leave classes. Attach your CSS transition property to the -active classes.
  • Prefer GPU-friendly properties like transform and opacity over layout-affecting properties like width or height.
  • Always include :key bindings on items in <TransitionGroup> to ensure Vue tracks elements correctly during animations.
  • Respect user motion preferences by checking prefers-reduced-motion: reduce and disabling or shortening animations for affected users.
  • Motion adds polish to production applications, but only when it communicates something meaningful to the user—don't animate for the sake of it.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles