CSS classes are the "autopilot" of animations — they work great for simple transitions. But sometimes you need full control over animations — e.g., dynamically computed values, data-driven animations, or integration with JS libraries. In such cases, Vue offers JavaScript animation hooks — like manual rocket steering instead of autopilot.
<Transition> emits events at each stage of the animation. You can listen to them:1<Transition
2 :css="false"
3 @before-enter="onBeforeEnter"
4 @enter="onEnter"
5 @after-enter="onAfterEnter"
6 @enter-cancelled="onEnterCancelled"
7 @before-leave="onBeforeLeave"
8 @leave="onLeave"
9 @after-leave="onAfterLeave"
10 @leave-cancelled="onLeaveCancelled"
11>
12 <div v-if="show">...</div>
13</Transition>
— called before the element is inserted into the DOM. Here you set the initial animation state (e.g., @before-enter(el)
opacity: 0, scale: 0.5).
— called on the next frame after the element is inserted. Here you run the actual animation. You must call @enter(el, done)
when the animation finishes!done()
— called after the animation finishes (after @after-enter(el)
done()). Here you clean up after the animation (remove inline styles, etc.).
— called when the enter animation is interrupted (e.g., the user quickly toggles the element).@enter-cancelled(el)
@before-leave(el) — initial state of leave, element is still visible@leave(el, done) — leave animation, you must call done()!@after-leave(el) — cleanup after leave@leave-cancelled(el) — leave animation interrupted (only with v-show)When using only JS hooks, add
:css="false" so Vue doesn't try to apply CSS classes or listen for transitionend/animationend events:1<Transition :css="false" @enter="onEnter" @leave="onLeave">
2 <div v-if="show">Control panel</div>
3</Transition>Without this attribute, Vue will wait for both the
done() callback and the CSS transition event — which can cause unpredictable behavior.done() is mandatory in the @enter and @leave hooks. It tells Vue that the animation has finished and it can proceed to the next step:1function onBeforeEnter(el) {
2 // Initial state — element invisible
3 el.style.opacity = 0
4 el.style.transform = 'translateY(20px)'
5}
6
7function onEnter(el, done) {
8 // Force reflow so the browser registers the initial state
9 el.offsetHeight
10
11 // Start the animation
12 el.style.transition = 'all 0.5s ease'
13 el.style.opacity = 1
14 el.style.transform = 'translateY(0)'
15
16 // Notify Vue when the animation finishes
17 el.addEventListener('transitionend', done, { once: true })
18}
19
20function onAfterEnter(el) {
21 // Cleanup — remove inline transition
22 el.style.transition = ''
23}Without calling
done(), Vue doesn't know when the animation has finished — in the case of @leave, the element will never be removed from the DOM.JS hooks allow dynamic values, which CSS classes cannot provide:
1function onEnter(el, done) {
2 // Dynamic animation direction based on data
3 const direction = el.dataset.direction || 'left'
4 const offset = direction === 'left' ? '-100px' : '100px'
5
6 el.style.opacity = 0
7 el.style.transform = `translateX(${offset})`
8 el.offsetHeight // reflow
9
10 el.style.transition = 'all 0.4s ease-out'
11 el.style.opacity = 1
12 el.style.transform = 'translateX(0)'
13
14 el.addEventListener('transitionend', done, { once: true })
15}| Use CSS classes | Use JS hooks | |---|---| | Simple fade, slide | Dynamic animation values | | Fixed durations | Data-dependent durations | | Typical transitions | Integration with GSAP, Anime.js | | Most cases | Complex animation sequences |