In Vue 3, how does `<Transition>` decide when a CSS enter or leave has finished, and when do you need its `type` or `duration` props?
answer
- computed style after the to class
- the longer of transition and animation
- end events from the element itself
- nested children need explicit timing
basics
~20 sVue 3's Transition reads the element's computed transition and animation delays and durations, then waits for matching end events from that element, with a fallback timer. Set type when both are used; set duration when nested children animate longer.
solid answer
~40 sOnce the `-to` class is on, `<Transition>` calls `getComputedStyle` on its root element, sums delay plus duration for CSS transitions and for CSS animations, and picks whichever finishes later. It then listens for `transitionend` or `animationend` events whose target is that element — one per listed property — and sets a fallback timer just past the computed total in case events never arrive. If no positive duration is declared, the phase ends at once. `type="transition"` or `type="animation"` forces which kind to wait for when an element uses both. `:duration`, a number or `{ enter, leave }` in milliseconds, replaces detection with a timer; you need it when inner elements animate longer or later than the root.
code
vue · 35 lines<script setup lang="ts">
import { ref } from 'vue'
const open = ref(false)
</script>
<template>
<button @click="open = !open">Details</button>
<Transition name="sheet" :duration="{ enter: 450, leave: 300 }">
<div v-if="open" class="sheet">
<div class="sheet-body">Order details</div>
</div>
</Transition>
</template>
<style>
.sheet-enter-active,
.sheet-leave-active {
transition: opacity 200ms ease;
}
.sheet-enter-active .sheet-body {
transition: transform 300ms ease 150ms;
}
.sheet-leave-active .sheet-body {
transition: transform 300ms ease;
}
.sheet-enter-from,
.sheet-leave-to {
opacity: 0;
}
.sheet-enter-from .sheet-body,
.sheet-leave-to .sheet-body {
transform: translateY(24px);
}
</style>go deeper
Know that Vue waits for the CSS transition or animation on the wrapped element before removing classes or the leaving element.
Explain the computed-style read, the root-only end events and the fallback timer, and when type or duration overrides them.
Diagnose truncated nested animations and instant toggles from a bad name, and keep duration values in step with the CSS they describe.
Prefer transitions whose timing lives on the root, or on shared duration tokens, so detection works without hand-kept magic numbers.
## Why Vue has to know when the animation ends A CSS-driven `<Transition>` must do two things at the end of each phase: remove the `-active` and `-to` classes, and — for a leave — **remove the element** from the DOM. If it did either too early, the animation would be cut off; too late, and a stale element would linger. Vue has no direct way to ask the browser "is this element still animating?", so it infers the end from the element's computed styles and from DOM events. ## The detection algorithm For a CSS-driven phase (no explicit `done` in your handler, `:css` not false): 1. On the frame where `-from` is swapped for `-to`, Vue reads `getComputedStyle(el)` for the root element. 2. It reads the comma-separated lists of `transition-delay` / `transition-duration` and `animation-delay` / `animation-duration`, and computes the longest delay-plus-duration for each kind. 3. Without a `type` prop it picks the **kind that finishes later**; with `type` it uses only that kind. 4. It adds a listener for `transitionend` or `animationend` and counts events **whose target is the element itself**, until it has seen one per listed duration. 5. It also sets a **fallback timer** a millisecond past the computed total, so a missing event cannot strand the phase. 6. If neither kind has a positive duration, it ends the phase immediately. Two consequences follow. Events bubbling from **descendants are ignored**, because only the root's own events count. And the timing is computed from the **root's** styles only; a child with a longer transition does not extend the phase. ## `type`: transition or animation An element can have both a CSS `transition` and a keyframe `animation` in its active class — say a 300 ms opacity transition and a 1 s bounce animation. Auto-detection already waits for the longer of the two, so `type` is only needed when you want the **other** one to decide the end, or when a stray inherited declaration of the other kind confuses the detection. `type="animation"` makes Vue listen only for `animationend` and use only the animation timings. ## `duration`: nested and delayed children The common reason to override detection is a **nested transition**: the root fades in quickly while an inner panel slides in later, with its own `transition-delay`. Because Vue only measures the root, it would end the enter after the root's short fade — removing `-enter-active` and cutting the inner slide mid-flight — or, on leave, remove the whole element before the inner animation finished. ```vue-html <Transition name="sheet" :duration="{ enter: 450, leave: 300 }"> <div v-if="open" class="sheet"><div class="sheet-body">…</div></div> </Transition> ``` With `:duration`, Vue skips style reading and event listening and ends each phase on a plain timer of that many milliseconds. A single number applies to both phases; an object sets them separately. ## The props at a glance | Situation | What to set | What Vue then does | |---|---|---| | Plain transition or plain animation | nothing | detects the kind and its timing | | Both kinds, the shorter should decide | `type` | listens only for that kind | | Children animate longer than the root | `:duration` | ends the phase on a timer | | Animation driven by JavaScript | a `done` parameter | waits for your `done()` call | ## Edge cases worth knowing - **No CSS at all.** A mistyped `name`, or styles that never loaded, means no duration is found; the element toggles instantly with no warning. - **Wrong declaration site.** A `transition` declared only on `-enter-from` has vanished by the time the computed style is read, so again nothing is detected. - **Missed events.** If the browser never dispatches the end event, the fallback timer still ends the phase, just a millisecond late. - **`duration` does not style anything.** It only tells Vue when to stop; the CSS still decides how the element actually animates, so the two must agree. When a designer later changes the CSS timing, a stale `:duration` either cuts the new animation short or leaves the element sitting idle before removal — one reason to keep durations in shared variables that both the CSS and the template read.
- Why does a nested slide get cut off when only the root has a short fade?Vue computes the timeout from the root's own computed styles and counts only end events whose target is the root. The child's `transitionend` bubbles up but is ignored, so the root's shorter fade ends the phase and the `-active` class comes off, snapping the child on enter or removing the element mid-animation on leave. An explicit `:duration` covering the child's delay plus duration fixes it.
- What happens if the element inside `<Transition>` has no transition or animation declared at all?Vue still adds the classes, but when it reads the computed styles it finds no positive duration and ends the phase immediately: enter classes come off and a leaving element is removed right away. That is how a mistyped `name` shows up — the content toggles instantly, with no warning in the console.
saying these in an interview costs you the question
- Vue ends the phase on the first transitionend from any descendant.
- Without a duration prop Vue waits a fixed default time before removing the element.
- The type prop chooses between CSS classes and JavaScript hooks.
- Vue warns when the element inside Transition has no CSS transition defined.
- The :duration prop sets the CSS transition-duration on the element.