# Transitions and Animations

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Advanced Components
Source: https://www.devtutorials.ai/learn/vue/advanced-components/transitions-and-animations

> The <Transition> component and its six CSS classes, named transitions, transition modes, CSS animations, custom class names, JavaScript hooks and :css="false", transitions between components, reusable transitions, <TransitionGroup> with keys, tag and move classes, state-driven animation and reduced motion.

## Movement Helps People Follow

- When something appears or disappears suddenly, people can miss it.
- A short fade or slide shows **what** changed and **where**.
- Vue's `<Transition>` adds CSS classes at the right moments when an element enters or leaves.
- You write the CSS. Vue decides when each class is on.

**Simple lesson:** `<Transition>` adds classes while an element enters or leaves. Your CSS animates them.

### A Real-World Example

- Meera's site shows a "Added to cart" message after each order.
- It should fade in, then fade out, not just pop.

const show = ref<boolean>(false);

function addToCart(): void {
    show.value = true;
    setTimeout(() => (show.value = false), 1500);
}
</script>

<template>
    <button @click="addToCart">Add Dosa to cart</button>
      <p v-if="show" class="toast">✓ Added to cart</p>
</template>

<style>
.toast { background: #e6f7ec; padding: 6px 10px; border-radius: 8px; display: inline-block; }
.fade-enter-active,
.fade-leave-active { transition: opacity 0.4s ease; }
.fade-enter-from,
.fade-leave-to { opacity: 0; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** the message faded in, waited, then faded out.
- `name="fade"` made Vue use classes that start with `fade-`.

### What You Will Learn

1. The six transition classes and when they apply.
2. Named transitions.
3. Transition modes for switching elements.
4. CSS keyframe animations.
5. Custom class names, for CSS libraries.
6. JavaScript hooks.
7. Transitions between components, and reusable transitions.
8. Animating lists with `<TransitionGroup>`.
9. Animating numbers from state, and respecting reduced motion.

## The Six Classes

- `<Transition>` works on **one** element or component that is added or removed by `v-if`, `v-show`, `:is` or a `key` change.
- Entering:
  - `v-enter-from`: the start. Added before the element appears, removed one frame later.
  - `v-enter-active`: the whole enter phase. Put the `transition` here.
  - `v-enter-to`: the end state.
- Leaving: `v-leave-from`, `v-leave-active`, `v-leave-to`.
- With `name="fade"`, `v-` becomes `fade-`.
- Around a component, that component must have **one** root element, or nothing animates.
- Add `appear` to animate the first show when the page loads, too.

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 180" role="img" aria-labelledby="fig-vue-trans" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-trans">Animation: a box fades in from opacity 0 while the classes fade-enter-from, fade-enter-active and fade-enter-to light up on a timeline, then fades out while the leave classes light up</title>
  <rect x="270" y="20" width="100" height="44" rx="10" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 2 }}><animate attributeName="opacity" values="0;0;1;1;1;0;0" keyTimes="0;0.05;0.35;0.5;0.55;0.85;1" dur="6s" repeatCount="indefinite" /></rect>
  <text x="320" y="47" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>toast<animate attributeName="opacity" values="0;0;1;1;1;0;0" keyTimes="0;0.05;0.35;0.5;0.55;0.85;1" dur="6s" repeatCount="indefinite" /></text>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700 }}>
    <rect x="20" y="90" width="90" height="26" rx="6" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} opacity="0.3"><animate attributeName="opacity" values="0.3;1;0.3;0.3" keyTimes="0;0.03;0.08;1" dur="6s" repeatCount="indefinite" /></rect><text x="65" y="107" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>enter-from</text>
    <rect x="115" y="90" width="190" height="26" rx="6" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} opacity="0.3"><animate attributeName="opacity" values="0.3;1;1;0.3;0.3" keyTimes="0;0.04;0.35;0.37;1" dur="6s" repeatCount="indefinite" /></rect><text x="210" y="107" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>enter-active (transition here)</text>
    <rect x="200" y="122" width="105" height="26" rx="6" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} opacity="0.3"><animate attributeName="opacity" values="0.3;0.3;1;1;0.3;0.3" keyTimes="0;0.08;0.09;0.35;0.37;1" dur="6s" repeatCount="indefinite" /></rect><text x="252" y="139" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>enter-to</text>
    <rect x="335" y="90" width="90" height="26" rx="6" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }} opacity="0.3"><animate attributeName="opacity" values="0.3;0.3;1;0.3;0.3" keyTimes="0;0.55;0.56;0.6;1" dur="6s" repeatCount="indefinite" /></rect><text x="380" y="107" textAnchor="middle" style={{ fill: "var(--ngc-orange)" }}>leave-from</text>
    <rect x="430" y="90" width="190" height="26" rx="6" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }} opacity="0.3"><animate attributeName="opacity" values="0.3;0.3;1;1;0.3;0.3" keyTimes="0;0.55;0.56;0.85;0.87;1" dur="6s" repeatCount="indefinite" /></rect><text x="525" y="107" textAnchor="middle" style={{ fill: "var(--ngc-orange)" }}>leave-active</text>
    <rect x="515" y="122" width="105" height="26" rx="6" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }} opacity="0.3"><animate attributeName="opacity" values="0.3;0.3;1;1;0.3;0.3" keyTimes="0;0.6;0.61;0.85;0.87;1" dur="6s" repeatCount="indefinite" /></rect><text x="567" y="139" textAnchor="middle" style={{ fill: "var(--ngc-orange)" }}>leave-to</text>
  </g>
  <text x="320" y="172" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 12 }}>Enter: from → to while active. Leave: from → to while active. Then Vue removes the element.</text>
</svg>
<figcaption>Vue switches the classes. The `transition` on the active class does the moving.</figcaption>
</figure>

### Slide and Fade Together

const open = ref<boolean>(false);
</script>

<template>
    <button @click="open = !open">{{ open ? "Hide" : "Show" }} today's offers</button>
      <ul v-show="open" class="offers">
        <li>Free lassi with every thali</li>
        <li>10% off on Sundays</li>
      </ul>
</template>

<style>
.slide-enter-active { transition: all 0.3s ease-out; }
.slide-leave-active { transition: all 0.2s ease-in; }
.slide-enter-from,
.slide-leave-to { transform: translateY(-12px); opacity: 0; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**Simple lesson:** put `transition` on `-active`, and the hidden look on `-enter-from` and `-leave-to`.

`<Transition>` only animates. It does not add or remove anything by itself. The `v-if`, `v-show` or `:key` inside it decides that.

## Switching Elements and Modes

- `<Transition>` can switch between elements with `v-if` / `v-else`, or by changing a `key`.
- By default, the old one leaves while the new one enters, at the same time.
- `mode="out-in"`: the old one leaves first, then the new one enters. Usually the nicest.
- `mode="in-out"`: the new one enters first. Rarely used.

const dishes: string[] = ["Masala Dosa", "Idli", "Pongal", "Vada"];
const index = ref<number>(0);
</script>

<template>
    <button @click="index = (index + 1) % dishes.length">Next special</button>
      <h3 :key="dishes[index]">{{ dishes[index] }}</h3>
</template>

<style>
.swap-enter-active,
.swap-leave-active { transition: all 0.25s ease; }
.swap-enter-from { opacity: 0; transform: translateX(20px); }
.swap-leave-to { opacity: 0; transform: translateX(-20px); }
</style>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** changing the `key` made Vue treat it as a new element, so it animated.

## CSS Animations and Custom Classes

- You can use `@keyframes` animations instead of transitions. Put `animation` on the `-active` class.
- Custom class props replace the default names: `enter-active-class`, `leave-active-class` and the rest.
- This is how you use a CSS animation library, such as Animate.css, with Vue.

const show = ref<boolean>(true);
</script>

<template>
    <button @click="show = !show">Toggle the badge</button>
      <span v-if="show" class="badge">🔥 Chef's special</span>
</template>

<style>
.badge { display: inline-block; background: gold; padding: 4px 10px; border-radius: 99px; }
.pop-in { animation: pop 0.4s; }
.pop-out { animation: pop 0.3s reverse; }
@keyframes pop {
    0% { transform: scale(0); }
    70% { transform: scale(1.2); }
    100% { transform: scale(1); }
}
</style>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**Simple lesson:** `mode="out-in"` for clean swaps. Custom classes connect CSS libraries.

## JavaScript Hooks

- `<Transition>` emits events at each step: `@before-enter`, `@enter`, `@after-enter`, `@enter-cancelled`, and the same for leave.
- `@enter(el, done)` and `@leave(el, done)` must call `done()` when the animation finishes.
- Add `:css="false"` to tell Vue not to look for CSS classes. It is faster for pure JavaScript animation.
- Here the browser's Web Animations API (`el.animate`) does the work.

const show = ref<boolean>(false);

function onEnter(el: Element, done: () => void): void {
    const animation = (el as HTMLElement).animate(
      [
        { transform: "rotate(-10deg) scale(0.5)", opacity: 0 },
        { transform: "rotate(0) scale(1)", opacity: 1 },
      ],
      { duration: 400, easing: "ease-out" },
    );
    animation.onfinish = () => done();
}

function onLeave(el: Element, done: () => void): void {
    const animation = (el as HTMLElement).animate([{ opacity: 1 }, { opacity: 0 }], { duration: 200 });
    animation.onfinish = () => done();
}
</script>

<template>
    <button @click="show = !show">Ring the bell</button>
      <p v-if="show" style="font-size: 28px">🔔</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

With `:css="false"`, you **must** call `done()` in `@enter` and `@leave`. If you forget, Vue never finishes the transition, and a leaving element is never removed.

## Components and Reusable Transitions

- `<Transition>` works around a component, or around `<component :is>` and `<RouterView>`'s slot.
- To reuse one transition everywhere, wrap it in your own component and pass the content through a slot.

const current = shallowRef(MenuTab);
</script>

<template>
    <button @click="current = MenuTab">Menu</button>
    <button @click="current = ReviewsTab">Reviews</button>
      <component :is="current" />
</template>
`,
    "FadeSwap.vue": `<template>
      <slot />
</template>

<style>
.fswap-enter-active,
.fswap-leave-active { transition: opacity 0.25s; }
.fswap-enter-from,
.fswap-leave-to { opacity: 0; }
</style>
`,
    "MenuTab.vue": `<template>
    <p>Dosa · Idli · Vada</p>
</template>
`,
    "ReviewsTab.vue": `<template>
    <p>⭐⭐⭐⭐⭐ "Lovely chutney"</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**Simple lesson:** JavaScript hooks for full control. A small wrapper component makes a transition reusable.

## TransitionGroup

- `<TransitionGroup>` animates items in a `v-for` list as they are added, removed or moved.
- Every item needs a unique `key`.
- `tag="ul"` makes the group render a real `<ul>` around the items.
- The extra `-move` class animates items sliding to their new place.
- Give leaving items `position: absolute`, so the others can slide smoothly.

let next = 4;
const orders = ref([
    { id: 1, dish: "Dosa" },
    { id: 2, dish: "Idli" },
    { id: 3, dish: "Vada" },
]);

function add(): void {
    orders.value.splice(1, 0, { id: next, dish: "Order " + next++ });
}

function remove(id: number): void {
    orders.value = orders.value.filter((o) => o.id !== id);
}

function shuffle(): void {
    orders.value = [...orders.value].sort(() => Math.random() - 0.5);
}
</script>

<template>
    <button @click="add">Add</button>
    <button @click="shuffle">Shuffle</button>
      <li v-for="o in orders" :key="o.id">
        {{ o.dish }} <button @click="remove(o.id)">✕</button>
      </li>
</template>

<style>
.orders { position: relative; padding-left: 18px; }
.list-move,
.list-enter-active,
.list-leave-active { transition: all 0.4s ease; }
.list-enter-from,
.list-leave-to { opacity: 0; transform: translateX(30px); }
.list-leave-active { position: absolute; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

## State-Driven Animation

- Some animations follow a **number**, not an element entering or leaving.
- Bind it to a style and add a CSS `transition`. The browser animates the change.
- Or move a number smoothly yourself with `requestAnimationFrame` in a watcher.

const orders = ref<number>(10);
const shown = ref<number>(10);

watch(orders, (to) => {
    const from = shown.value;
    const start = performance.now();
    function step(now: number): void {
      const t = Math.min(1, (now - start) / 500);
      shown.value = Math.round(from + (to - from) * t);
      if (t < 1) requestAnimationFrame(step);
    }
    requestAnimationFrame(step);
});
</script>

<template>
    <div class="bar" :style="{ width: orders * 4 + 'px' }"></div>
    <p>Orders today: {{ shown }}</p>
    <button @click="orders += 15">+15 orders</button>
    <button @click="orders = 10">Reset</button>
</template>

<style>
.bar { height: 16px; background: #2f9e6e; border-radius: 8px; transition: width 0.5s ease; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

Some people feel sick from motion. Respect their system setting with CSS: `@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }`. Keep transitions short, around 150 to 400 ms.

**Simple lesson:** `<TransitionGroup>` animates lists. For numbers, animate the style or the value.

## TL;DR

- **`<Transition>`** = animates one element entering or leaving.
- **Six classes** = `v-enter-from/active/to`, `v-leave-from/active/to`.
- **`name="fade"`** = class names start with `fade-`.
- **`mode="out-in"`** = the old element leaves before the new one enters.
- **Custom classes** = `enter-active-class` and friends, for CSS libraries.
- **JavaScript hooks** = `@enter(el, done)`, `@leave(el, done)`, `:css="false"`.
- **`<TransitionGroup>`** = animates `v-for` lists. Needs keys. `tag`, `-move`.
- **State-driven** = bind styles with a CSS transition, or tween the number.
- **Reduced motion** = turn animations off for `prefers-reduced-motion`.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Two elements in one `<Transition>` | It animates both | One root only. Use `v-if`/`v-else` or `:key` | A warning, no animation |
| `transition` on `-enter-from` | Any class works | Put `transition` on `-active` | The element jumps without animating |
| No `key` when content changes | Vue sees the change | Add `:key` so Vue sees a new element | No animation on text change |
| Forgetting `done()` | Vue knows when JS ends | Call `done()` in `@enter`/`@leave` | Leaving elements stay forever |
| No `position: absolute` on leaving list items | Lists slide by themselves | Add it to `-leave-active` | Items jump instead of sliding |
| Long, flashy animations | More motion is better | 150 to 400 ms, respect reduced motion | Slow, tiring, or sickening pages |

</div>

## Check Yourself

## Exercises

## What to Remember

- `<Transition>` adds enter and leave classes. Your CSS does the animation.
- Put `transition` on `-active`, the hidden look on `-enter-from` and `-leave-to`.
- Use `mode="out-in"` and `:key` for clean swaps.
- JavaScript hooks must call `done()`. `<TransitionGroup>` animates keyed lists.
- Keep motion short, and respect reduced motion.

Next: **Render Functions and Component Patterns**
