| |

Vue.js 66 🟢 CSS Animation Classes, JavaScript Hooks, and Mode Configurations (in-out, out-in)

⏱️ Estimated read: 15 min | |

Vue’s <Transition> component is a timing coordinator, not an animation library. It does not define how an element should fade, slide, or bounce—it defines when the classes or hooks that do those things are applied. The component wraps a single element or component, watches for its insertion or removal, and at precise moments adds and removes CSS classes or calls JavaScript hooks. The animation itself lives in the stylesheet or in the hook function. Understanding the three mechanisms—the six CSS classes, the JavaScript hook lifecycle, and the mode prop—is understanding the entire transition system.

The CSS classes are the declarative path. For a transition named fade, Vue applies .fade-enter-from, .fade-enter-active, and .fade-enter-to during insertion, and the corresponding leave classes during removal . The -active class defines the transition’s duration and easing; the -from and -to classes define the start and end states. The JavaScript hooks are the imperative path. When CSS cannot express the animation—a staggered sequence, an animation library like GSAP, or a transition that must be cancelled mid-flight—the hooks provide the entry points and the done callback signals completion . The mode prop is the sequencing control. It determines whether the entering element and the leaving element animate simultaneously, or whether one waits for the other to finish .

Key point: Vue applies six CSS classes (-enter-from, -enter-active, -enter-to, -leave-from, -leave-active, -leave-to) at precise moments during insertion and removal, and the -active class is where the transition duration belongs. JavaScript hooks (@before-enter, @enter, @after-enter, etc.) provide the imperative path, and the done callback in @enter and @leave tells Vue when a JavaScript-only transition has finished. The mode prop controls sequencing: out-in waits for the old element to leave before the new one enters, in-out does the reverse, and the default animates both simultaneously.


Why the transition system is split into classes and hooks

The CSS-first problem. Most transitions are simple: fade an element in, slide it out, scale it up. These are expressible in CSS, and CSS transitions and animations are hardware-accelerated and declarative. The class-based system lets the developer write the animation in the stylesheet, where it belongs, and the component only manages the timing . The alternative—a JavaScript animation library for every fade—adds bundle size and complexity for no benefit.

The imperative problem. CSS cannot express every animation. A sequence where three elements enter with different delays, a transition that must be cancelled when the user navigates away, or an animation driven by a physics engine requires JavaScript. The hooks provide the lifecycle: @before-enter runs before the element is inserted, @enter runs after insertion with a done callback, and @after-enter runs when the transition is complete . When the hooks are used without CSS, the :css="false" prop tells Vue to skip class detection and rely entirely on done .

The sequencing problem. When two elements swap—a v-if and a v-else, or a dynamic component with a changing key—both are present during the transition by default. The leaving element and the entering element overlap, which can produce a visual mess if the transition involves movement or size changes. The mode prop controls the sequence: out-in waits for the leaving element to finish before the entering element starts, and in-out does the opposite . The default is simultaneous, which is correct for crossfades but wrong for slides.

The duration problem. Vue needs to know when a CSS transition ends. It listens for the first transitionend or animationend event on the root element. If the element has nested transitions, or if a transition and an animation are both running, the first event may fire before the full effect is complete. The duration prop provides an explicit duration, and the type prop tells Vue whether to listen for transitionend or animationend when both are present .


a. The six CSS classes and their timing

The transition name is the prefix. A <Transition name="fade"> produces .fade-enter-from, .fade-enter-active, .fade-enter-to, and the three leave equivalents . The class names follow a precise sequence.

<script setup>
import { ref } from 'vue'
// The flag that controls the element's presence
const show = ref(false)
</script>

<template>
  <!-- The Transition wraps the conditional element -->
  <!-- name="fade" sets the class prefix to .fade-* -->
  <Transition name="fade">
    <p v-if="show">Hello</p>
  </Transition>
  <button @click="show = !show">Toggle</button>
</template>

<style>
/* The -active class defines the transition itself */
/* It is applied for the entire enter or leave phase */
.fade-enter-active,
.fade-leave-active {
  /* The duration and easing belong here */
  transition: opacity 0.5s ease;
}

/* The -from and -to classes define the start and end states */
/* The transition interpolates between them */
.fade-enter-from,
.fade-leave-to {
  /* The element starts (or ends) transparent */
  opacity: 0;
}
</style>

The -active class is the one that carries the transition property. The -from class is applied before the element is inserted (for enter) or immediately when the leave is triggered. On the next frame, the -from class is removed and the -to class is applied, and the browser interpolates between the two states defined by the -active transition . The -to class is often empty because the element’s natural state is the end state; the transition runs from the -from state to the natural state.

The timing is the detail that matters. The -enter-from class is removed one frame after the element is inserted, which is what triggers the transition. If the -from class and the natural state are the same, no transition runs. This is why the -from class must define a different state—opacity 0, a transform, a color—that the browser can interpolate from.

For CSS animations (as opposed to transitions), the -enter-from class is removed on the animationend event rather than one frame after insertion . The practical difference is that the animation’s keyframes define the entire progression, and the -active class applies the animation property.


b. The JavaScript hooks and the done callback

The hooks are the imperative alternative. They are useful when the animation is driven by a library, when the transition must be cancelled, or when CSS cannot express the effect.

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

const show = ref(false)

// The before-enter hook runs before the element is inserted
// It sets the initial state directly on the element
function onBeforeEnter(el) {
  el.style.opacity = 0
}

// The enter hook runs after insertion
// The done callback tells Vue the transition is complete
function onEnter(el, done) {
  gsap.to(el, {
    opacity: 1,
    duration: 0.5,
    // The onComplete callback calls done
    // Without this, Vue waits indefinitely
    onComplete: done,
  })
}

// The leave hook runs when the element is leaving
function onLeave(el, done) {
  gsap.to(el, {
    opacity: 0,
    duration: 0.5,
    onComplete: done,
  })
}
</script>

<template>
  <!-- :css="false" tells Vue to skip CSS class detection -->
  <!-- The hooks are the only mechanism -->
  <Transition
    :css="false"
    @before-enter="onBeforeEnter"
    @enter="onEnter"
    @leave="onLeave"
  >
    <div v-if="show">Animated content</div>
  </Transition>
  <button @click="show = !show">Toggle</button>
</template>

The done callback is mandatory when :css="false" is set. If @enter does not call done, Vue does not know the transition is finished, and the element remains in the transition state . The done callback is also available in @leave, and it is called automatically if the hook does not accept a second argument—but when the hook is asynchronous, the explicit done is required.

The @enter-cancelled hook fires if the enter transition is interrupted before it completes. This is the cleanup opportunity: cancel the animation library’s tween, clear the timer, remove the inline styles. The same applies to @leave-cancelled, which is available only for v-show transitions .


c. The mode prop: in-out, out-in, and the default

The mode prop is the sequencing decision. It is only relevant when two elements are transitioning at the same time—a v-if/v-else swap, or a dynamic component with a changing key.

<script setup>
import { ref, computed } from 'vue'
import ViewA from './ViewA.vue'
import ViewB from './ViewB.vue'

const view = ref('A')
// The component to render based on the view
const current = computed(() => (view.value === 'A' ? ViewA : ViewB))
</script>

<template>
  <!-- mode="out-in" waits for the old view to leave -->
  <!-- then the new view enters -->
  <!-- The key forces Vue to treat the components as different -->
  <Transition name="fade" mode="out-in">
    <component :is="current" :key="view" />
  </Transition>
  <button @click="view = 'A'">A</button>
  <button @click="view = 'B'">B</button>
</template>

The three modes have distinct visual effects. out-in waits for the leaving element to complete its leave transition before the entering element begins its enter transition . This is the correct choice for slides, where two elements moving at the same time would overlap awkwardly. in-out does the reverse: the new element enters first, and the old element leaves after the enter completes. This is less common but useful when the new content should establish itself before the old content disappears. The default mode runs both transitions simultaneously, which is correct for crossfades where the two elements overlap in the same space and the opacity transition hides the swap.

The key is not optional. Without the key, Vue reuses the same element and the transition does not fire, because the element is not actually being inserted or removed—it is being patched. The :key="view" tells Vue that the two views are different elements, which is what triggers the leave and enter phases .


Complete Example Session

The following session builds a view switcher with an out-in transition, a list with staggered CSS animation classes, and a modal with a JavaScript-driven leave hook.

<!-- ============================================
     PART 1: THE VIEW SWITCHER (out-in)
     ============================================ -->
<script setup>
import { ref, computed } from 'vue'
import ViewA from './ViewA.vue'
import ViewB from './ViewB.vue'

const view = ref('A')
const current = computed(() => (view.value === 'A' ? ViewA : ViewB))
</script>

<template>
  <!-- out-in: old view leaves, then new view enters -->
  <!-- The key forces the component swap -->
  <Transition name="slide" mode="out-in">
    <component :is="current" :key="view" />
  </Transition>
  <button @click="view = 'A'">A</button>
  <button @click="view = 'B'">B</button>
</template>

<style>
/* The leave transition slides the old view out */
.slide-leave-active {
  transition: transform 0.3s ease;
}
.slide-leave-to {
  transform: translateX(-100%);
}

/* The enter transition slides the new view in */
.slide-enter-active {
  transition: transform 0.3s ease;
}
.slide-enter-from {
  transform: translateX(100%);
}
</style>
<!-- ============================================
     PART 2: THE STAGGERED LIST (CSS classes)
     ============================================ -->
<script setup>
import { ref } from 'vue'
const items = ref([1, 2, 3])
</script>

<template>
  <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item">{{ item }}</li>
  </TransitionGroup>
  <button @click="items.push(items.length + 1)">Add</button>
</template>

<style>
/* The enter-active class applies the animation */
/* The animation-delay creates the stagger */
.list-enter-active {
  animation: slide-in 0.5s ease;
}
.list-leave-active {
  animation: slide-in 0.5s ease reverse;
}
@keyframes slide-in {
  from {
    opacity: 0;
    transform: translateX(30px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}
</style>
<!-- ============================================
     PART 3: THE JAVASCRIPT MODAL (hooks + done)
     ============================================ -->
<script setup>
import { ref } from 'vue'
import gsap from 'gsap'

const show = ref(false)

// Set the initial state before insertion
function onBeforeEnter(el) {
  el.style.opacity = 0
  el.style.transform = 'scale(0.8)'
}

// Animate the enter and call done on complete
function onEnter(el, done) {
  gsap.to(el, {
    opacity: 1,
    scale: 1,
    duration: 0.3,
    onComplete: done,
  })
}

// Animate the leave and call done on complete
function onLeave(el, done) {
  gsap.to(el, {
    opacity: 0,
    scale: 0.8,
    duration: 0.3,
    onComplete: done,
  })
}
</script>

<template>
  <!-- :css="false" means the hooks drive the transition -->
  <Transition
    :css="false"
    @before-enter="onBeforeEnter"
    @enter="onEnter"
    @leave="onLeave"
  >
    <div v-if="show" class="modal">Modal content</div>
  </Transition>
  <button @click="show = !show">Toggle Modal</button>
</template>

The three parts cover the out-in view switcher, the staggered list with CSS animation classes, and the JavaScript-driven modal with the done callback. What the session demonstrates is that the mode prop controls sequencing, the CSS classes control declarative animation, and the hooks control imperative animation.


Quick Reference

The Six CSS Classes

ClassApplied When
{name}-enter-fromBefore enter, removed one frame after insertion
{name}-enter-activeDuring entire enter phase
{name}-enter-toOne frame after insertion, removed at end
{name}-leave-fromImmediately when leave starts
{name}-leave-activeDuring entire leave phase
{name}-leave-toOne frame after leave starts, removed at end

JavaScript Hooks

HookReceivesPurpose
@before-enterelSet initial state
@enterel, doneRun animation, call done
@after-enterelCleanup after enter
@leaveel, doneRun leave animation, call done
@after-leaveelCleanup after leave
@enter-cancelledelCleanup interrupted enter

Mode Configurations

ModeBehaviorUse Case
defaultBoth simultaneousCrossfade
out-inOld leaves, then new entersSlide, swap
in-outNew enters, then old leavesEstablish new first

Key Props

PropDefaultPurpose
name"v"Class prefix
csstrueApply CSS classes
typeauto"transition" or "animation"
durationautoExplicit duration in ms

Best Practices

✅ Do This:

<!-- Use out-in for slides -->
<Transition name="slide" mode="out-in">...</Transition>

<!-- Set the initial state in before-enter -->
@before-enter="el => el.style.opacity = 0"

<!-- Call done in the enter hook when using JS -->
@enter="(el, done) => gsap.to(el, { opacity: 1, onComplete: done })"

<!-- Use :css="false" for JS-only transitions -->
<Transition :css="false" @enter="onEnter">

❌ Don’t Do This:

<!-- Forget the key in mode transitions -->
<component :is="view" /> <!-- no transition -->

<!-- Forget done in JS transitions -->
@enter="el => gsap.to(el, { opacity: 1 })" <!-- hangs -->

<!-- Use in-out for crossfades -->
mode="in-out" <!-- overlap looks wrong -->

Common Pitfalls

PitfallWhy It HappensFix
Transition does not fireElement not conditional or keyedv-if or :key
Both elements visibleNo modemode="out-in"
JS transition hangsdone not calledCall done on complete
CSS class not appliedWrong prefixMatch name to CSS
Duration ignoredNested transitionsExplicit duration

Real-World Examples

1. Fade transition

<Transition name="fade"><div v-if="show" /></Transition>

2. Slide out-in

<Transition name="slide" mode="out-in"><component :is="view" :key="view" /></Transition>

3. JS enter hook

<Transition :css="false" @enter="(el, done) => gsap.to(el, { opacity: 1, onComplete: done })">

4. Staggered CSS animation

.list-enter-active { animation: slide-in 0.5s ease; }

5. Before-enter initial state

<Transition @before-enter="el => el.style.opacity = 0">

6. Cancel hook

<Transition @enter-cancelled="cancelAnimation">

7. Custom class

<Transition enter-active-class="animate__animated animate__tada">

8. Explicit duration

<Transition :duration="550">...</Transition>

9. Separate durations

<Transition :duration="{ enter: 500, leave: 800 }">...</Transition>

10. Type prop for both

<Transition type="animation">...</Transition>

Visual

Class Application Sequence

┌──────────────────────────────────────────────┐
│  ENTER                                       │
│  -enter-from added                           │
│  element inserted                            │
│  -enter-active added                         │
│  next frame: -enter-from removed             │
│  -enter-to added                             │
│  transitionend: -enter-active/-to removed    │
│                                              │
│  LEAVE                                       │
│  -leave-from added                           │
│  next frame: -leave-from removed             │
│  -leave-active added                         │
│  -leave-to added                             │
│  transitionend: element removed              │
└──────────────────────────────────────────────┘

Mode Comparison

┌──────────────────────────────────────────────┐
│  default: both simultaneous                  │
│  [old leaving] [new entering]                │
│                                              │
│  out-in: old leaves, then new enters         │
│  [old leaving] → [new entering]              │
│                                              │
│  in-out: new enters, then old leaves         │
│  [new entering] → [old leaving]              │
└──────────────────────────────────────────────┘

JS Hook Lifecycle

┌──────────────────────────────────────────────┐
│  @before-enter → set initial state           │
│         │                                    │
│         ▼                                    │
│  element inserted                            │
│         │                                    │
│         ▼                                    │
│  @enter(el, done) → animate, call done       │
│         │                                    │
│         ▼                                    │
│  @after-enter → cleanup                      │
└──────────────────────────────────────────────┘

Summary

ItemValue
CSS classes6 per transition (enter/leave × from/active/to)
-active classDefines transition duration
-from classDefines start state
JS hooks@before-enter, @enter, @after-enter, etc.
done callbackSignals JS transition completion
:css="false"Skips CSS class detection
mode="out-in"Old leaves, then new enters
mode="in-out"New enters, then old leaves
keyRequired for component swaps

Key takeaways:

  • The six CSS classes are the timing contract. Vue applies -enter-from, -enter-active, and -enter-to at precise moments, and the -active class is where the transition duration belongs .
  • JavaScript hooks are for animations CSS cannot express. The @enter and @leave hooks receive a done callback, and :css="false" tells Vue to rely on it instead of CSS events .
  • done must be called. A JavaScript transition that does not call done leaves the element in the transition state indefinitely .
  • mode controls sequencing. out-in waits for the old element to leave before the new one enters. in-out does the reverse. The default animates both simultaneously .
  • The key is what triggers the swap. Without it, Vue patches the same element and the transition does not fire .
  • The duration prop handles nested transitions. When the first transitionend event fires before the full effect is complete, an explicit duration tells Vue when the transition actually ends .

Remember: Vue’s <Transition> is a timing coordinator. The CSS classes and the JavaScript hooks are two ways to define the animation, and the mode prop is the sequencing control. The classes are declarative and belong in the stylesheet; the hooks are imperative and belong in the component when the animation is driven by a library or needs cancellation. The done callback is the contract for JavaScript transitions, and the key is the trigger for component swaps. The component does not animate anything itself—it decides when the animation starts and ends, and the animation does the rest.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!