Two elements fight for one slot until out-in serializes them
You swap a login form for a sign-up form with v-if. Both branches render a card of similar height, mode is unset, and the two cards animate at the same time: for the full 250ms leave the old card is still in the document while the new card is already inserted, so the container briefly holds two cards, its height doubles, and the content below jumps down and back up. The opacity animation itself looks fine. What bothers the user is the sequencing — and sequencing is exactly what the mode prop controls.
With no mode, <Transition> runs enter and leave simultaneously: both nodes are in the DOM, both class sequences run, and the swap finishes when the slower phase finishes. That default is harmless for an overlay taken out of flow, and wrong for anything sitting in normal document flow.
| mode | old element | new element | both in the DOM? | time until the swap is settled |
|---|---|---|---|---|
| default (none) | leaves immediately | enters immediately | the whole time | max(enter, leave) |
out-in |
leaves first, then is removed | waits, then enters | never | leave + enter |
in-out |
stays mounted, leaves after the new one finishes entering | enters immediately | during the enter phase | enter + leave |
mode="out-in" is the fix for in-flow content. Its timeline:
- You toggle the state. Vue does not render the new branch yet — it renders a placeholder into the slot and marks the component as leaving.
- The old element runs the full leave sequence:
-leave-from, then-leave-activewith-leave-to. - When the leave completes, Vue's internal
afterLeavefor that element clears the leaving flag and schedules a component update. - That update finally inserts the new child, which runs the enter sequence.
<Transition name="card" mode="out-in">
<LoginForm v-if="tab === 'login'" key="login" />
<SignupForm v-else key="signup" />
</Transition>
.card-enter-active,
.card-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.card-enter-from { opacity: 0; transform: translateY(8px); }
.card-leave-to { opacity: 0; transform: translateY(-8px); }
Two consequences to state before you choose it. First, total time is leave duration plus enter duration: two 250ms phases produce a 500ms swap, so out-in always feels slower even though nothing overlaps. On a control the user clicks repeatedly, cut both sides to 100–150ms rather than importing the 400ms timing you used for a one-shot alert. Second, between step 2 and step 4 there is no element in the slot: a focused input inside the old branch loses focus, and assistive technology hears nothing until the new branch mounts. If the swap is keyboard-driven, restore focus in @after-enter — never with a setTimeout that guesses the duration.
One condition gates the whole mechanism: the mode only sequences a real replacement, meaning the slot's child must change identity. That happens with different v-if branches (the compiler assigns each branch its own key), a changed key, or a different component behind <component :is>. If the same node is patched in place, there is no leave and no enter to order. The Transition Component already covered the six class phases; mode is one prop layered on top of that machinery, and it is the first thing to reach for when a toggle makes your layout jump.
Interviewers like this one: "How does Vue know when to insert the incoming element in out-in mode?" The answer is not a timer — Vue hooks the outgoing element's leave completion and re-renders from there, which is why a longer leave genuinely delays the new content.
Keeping both elements on screen with in-out and absolute positioning
Some swaps should overlap. A dashboard pane that crossfades between a chart and a table should never show an empty slot: the new pane arrives while the old one is still visible, and the old one disappears only after the new one has landed. That is mode="in-out".
The order is inverted relative to out-in. The new element is inserted immediately and runs its enter sequence. The old element stays mounted during that window, and Vue defers its leave until the new element's enter has completed — then the old element animates out and is removed. Both elements therefore coexist on screen for the whole enter phase, which is the overlapping sequence the mode exists for. No gap, no empty container, at the price of a period where two children are laid out at once.
That price is a layout problem. Two in-flow children mean the container's height is the sum of both panes, and everything below shifts. You have two coordinated options:
<template>
<div class="stage">
<Transition name="crossfade" mode="in-out">
<ChartPane v-if="view === 'chart'" key="chart" />
<TablePane v-else key="table" />
</Transition>
</div>
</template>
<style>
.stage {
position: relative;
min-height: 320px; /* absolute children cannot size their container */
}
.stage > * {
position: absolute;
inset: 0;
}
.crossfade-enter-active,
.crossfade-leave-active {
transition: opacity 0.3s ease;
}
.crossfade-enter-from,
.crossfade-leave-to {
opacity: 0;
}
</style>
Take the transitioning roots out of flow with position: absolute, and neither the entering nor the leaving node can push the page around — this is the same fix the docs apply to the default simultaneous mode, and it is why absolute positioning of the root is what makes overlapping enter and leave safe. The trade: an absolutely positioned child no longer contributes height, so the container must be given a size (min-height, aspect ratio, or a grid track), otherwise the stage collapses to zero and the panes paint outside it.
Choosing between the three modes comes down to one question — what should the container show during the swap?
| You want | Mode | Layout requirement |
|---|---|---|
| Old content gone before new arrives, no overlap | out-in |
none; in-flow is fine (expect an empty gap) |
| New content visible while old is still there | in-out |
take both roots out of flow |
| Cheapest possible swap of an out-of-flow overlay | default | none (absolute/toast already out of flow) |
Timing differs too: with in-out, the old element is only removed enter + leave after the toggle, so a slow enter keeps stale content visible longer. If the entering pane mounts async data, prefer out-in or the default — you do not want a half-loaded chart sitting on top of the table it replaces.
Rapid toggling deserves its own note. Click a third view while the overlap is still running, and the swap effectively restarts from whatever is mounted: the element mid-enter now has to leave, the element whose leave was still waiting gets removed early instead of animating, and you should expect one cycle of asymmetric motion. The end state always follows your latest value — transitions never win against state — but keep overlap windows short (200–300ms) so an impatient clicker converges on the right pane quickly.
Interview framing: ask for the difference between in-out and out-in and answer with the timeline, not the adjectives — which element moves first, when the two nodes coexist, and what that does to document flow. The layout consequence is the part most candidates miss.
What appear animates, and the four times mode stays silent
A hero banner that should fade in on page load does nothing: the component mounts, the markup is simply there, and no enter classes ever appear. That is expected — without help, <Transition> animates only toggles that happen after mount. The first insertion is silent. Adding the appear attribute makes the initial insertion run the enter sequence too.
appear does not invent a second class system. On first render Vue runs the appear variants of the hooks, and each one falls back to its enter counterpart when you have not overridden it:
First render with appear |
Falls back to | Class used unless overridden |
|---|---|---|
@before-appear |
@before-enter |
appear-from (default: enter-from) |
@appear |
@enter |
appear-active (default: enter-active) |
@after-appear |
@after-enter |
appear-to (default: enter-to) |
@appear-cancelled |
@enter-cancelled |
— |
So <Transition name="hero" appear> animates on load using your existing hero-enter-* rules, and a handler written as @enter="onEnter" also covers the first paint without a second registration. To give the load a different feel — slower, larger offset — pass appear-from-class, appear-active-class, and appear-to-class (or override with @before-appear/@appear in JavaScript).
<Transition name="page" appear>
<main v-if="loaded" :key="route.path" class="route-view">
<RouterView />
</main>
</Transition>
Remember the boundary: appear fires once, when that element is first inserted into the DOM. If loaded starts false and flips to true a second later, that is a normal post-mount enter — same classes, no appear involved. appear also does not replay on re-render or on props updates; it is keyed to insertion, not to painting.
Two refinements make appear production-safe. First, the first paint runs on every page load for every user, so this is the animation to wire to prefers-reduced-motion: a 600ms slide that fires on load is the effect most likely to bother someone, and unlike a toggle they cannot choose to skip it. Second, appear composes with modes — <Transition mode="out-in" appear> around <component :is="view"> animates the first view on mount and every later swap after it, and because the appear classes fall back to the enter classes, you can ship the load animation by adding a single attribute and no extra CSS.
Then there are four situations where a mode prop silently does nothing, which interviewers use to separate people who have run the code from people who have read about it:
v-showtoggling. The same element is hidden and shown; there is no incoming node to sequence against the outgoing one. Mode needs a replacement to orchestrate.<TransitionGroup>. The prop is not declared on the group at all — lists animate each child independently, which is the whole point of Animating Lists with TransitionGroup.- Content patching instead of replacing. Text or props change inside the same keyed element, so no enter or leave fires and there is nothing for a mode to order.
- A leave that never completes. In
out-in, the new branch is inserted only after the old one's leave finishes. A JavaScript leave hook declared with(el, done)that never callsdonefreezes the sequence forever — the mode looks broken while the real bug is a missing callback. You will meet that contract in JavaScript Transition Hooks.
The practical rule: use appear for first-paint feedback, and treat mode as sequencing for node replacements only. When a mode "does not work", check whether a replacement is actually happening before you touch the CSS.
Quiz
1. A <Transition mode="out-in"> wraps two keyed v-if branches whose CSS phases are 200ms leave and 300ms enter. How long after the toggle does the new branch finish entering?
2. A <Transition> with no mode wraps two in-flow cards of similar height. What does the user see during the swap?
3. You add the appear attribute to a <Transition> around a hero that renders on first load. Which statement is accurate?
4. A developer reports that mode="out-in" never shows the second view when a button toggles between two views controlled by v-show. Why?
Flashcards
Question
What does mode="out-in" sequence, and what is the hand-off point between the two elements?
Click to reveal answer
Answer
The leaving element finishes its leave phase first; Vue renders a placeholder instead of the new child, then the leaving element's afterLeave clears the internal leaving flag and re-renders, which inserts the incoming element and starts its enter. Total swap time is leave + enter, and the two nodes never coexist.
Question
How does mode="in-out" order the phases, and what layout does it demand?
Click to reveal answer
Answer
The new element is inserted and enters immediately while the old one stays mounted; Vue defers the old element's leave until the new one's enter completes, then removes it. Both nodes are in the DOM during the overlap, so the transitioning roots should be position: absolute (with the container sized explicitly) to avoid layout shift.
Question
What happens when a <Transition> has no mode at all?
Click to reveal answer
Answer
Enter and leave run simultaneously: both nodes are in the DOM, both class sequences run, and the swap settles after the slower phase — max(enter, leave). For in-flow content that doubles the container height during the swap, which is why modes or out-of-flow positioning exist.
Question
Which classes does appear use on first render, and how do you override them?
Click to reveal answer
Answer
By default the appear phase reuses the enter classes — appear-from/-active/-to fall back to enter-from/-active/-to — and the @before-appear/@appear/@after-appear hooks fall back to the enter handlers. Pass appear-from-class, appear-active-class, appear-to-class (or dedicated @appear handlers) for a different first-paint effect.
Question
Name two situations where a Transition mode prop silently does nothing.
Click to reveal answer
Answer
(1) v-show toggling — the same node persists, so there is no replacement to sequence. (2) TransitionGroup — the mode prop is not declared on it and lists animate children independently. Also: content patched in place (no enter/leave fires at all) and a leave that never completes (e.g. a JS leave hook that never calls done freezes mode="out-in" before the insert).
Revision Notes
Key Takeaways
- 1. The default mode is simultaneous — both nodes are in the DOM and in-flow content shifts; out-in trades that jump for an empty gap and a longer (leave + enter) swap
- 2. out-in hands off through the old element's afterLeave, not a timer: Vue holds back the new branch until the leave genuinely completes
- 3. in-out overlaps on purpose — the new element enters while the old stays mounted, so absolute positioning of both roots is what keeps the layout still
- 4. appear animates only the first insertion, and its classes and hooks fall back to the enter family, so one set of CSS covers both load and toggle
- 5. A mode needs a node replacement to sequence; v-show, patched nodes, TransitionGroup, and stalled leave hooks are the four ways it silently does nothing
Interview Tips
- • "Explain the three transition modes" — give timelines, not adjectives: default = simultaneous (max duration, nodes coexist), out-in = serialized via afterLeave (leave + enter), in-out = new enters first and the old leaves afterward; then add the layout consequence of coexistence
- • "Why does my out-in toggle feel slow?" — total time is leave + enter with an empty slot in between; shorten both phases or switch to in-out with absolute positioning if the gap is unacceptable
- • "When would you use appear?" — first-paint feedback on initial insertion; note it does not replay on re-render and that it reuses the enter classes and handlers by default
- • "A mode prop seems ignored — how do you debug?" — verify the child is actually being replaced (v-if/key/component swap, not v-show or a text patch), then check whether the leave phase can complete at all, including JS hooks that never call done
Cheat Sheet
Transition Modes and appear
Mode semantics (one element in, one element out):
| Mode | First | Then | Nodes coexist? | Swap duration |
|---|---|---|---|---|
| default | leave + enter simultaneously | settle when slower phase ends | yes, always | max(enter, leave) |
out-in |
old leaves; placeholder rendered | afterLeave re-renders, new enters | never | leave + enter |
in-out |
new inserted, enters immediately | old leaves after new's enter completes | during enter | enter + leave |
Layout rule: overlapping modes (default, in-out) need both transitioning roots position: absolute inside a sized position: relative container, otherwise the container's height is the sum of both nodes and the page jumps.
<Transition name="card" mode="out-in">
<LoginForm v-if="tab === 'login'" key="login" />
<SignupForm v-else key="signup" />
</Transition>
appear (first insertion only):
| Hook | Falls back to | Classes |
|---|---|---|
@before-appear |
@before-enter |
appear-from (default enter-from) |
@appear |
@enter |
appear-active (default enter-active) |
@after-appear |
@after-enter |
appear-to (default enter-to) |
@appear-cancelled |
@enter-cancelled |
— |
Mode does nothing when: the child is not replaced (v-show, patched content), the component is <TransitionGroup> (no mode prop), or the leave phase never completes (JS hook that never calls done).