React

Transitions in Vue 3: CSS animation without a library

Transition and TransitionGroup add and remove CSS classes as a node enters, leaves or moves: six lines of CSS cover a modal or a list.

Key points

  • Transition animates nothing by itself: it applies classes and leaves the duration to CSS.
  • Enter and leave have three phases: from, active, to. Duration belongs to active only.
  • TransitionGroup needs stable :key values, or the animated reorder never starts.

In Vue 3 you animate a node without installing anything: <Transition> adds and removes CSS classes as the element enters or leaves, and the browser does the rest. The component holds no keyframes.

The classes Vue adds


import { ref } from "vue";

const open = ref(false);



  Toggle

  
    Saved.
  



.fade-enter-active,
.fade-leave-active {
  transition: opacity 180ms ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

With name="fade" the classes become fade-enter-from, fade-enter-active and fade-enter-to, plus the three equivalents for leaving. Duration belongs on the active class only: that one stays applied for the whole transition.

Lists: TransitionGroup


import { ref } from "vue";

const items = ref([
  { id: 1, name: "Ada" },
  { id: 2, name: "Linus" },
]);



  
    {{ item.name }}
  



.list-move,
.list-enter-active,
.list-leave-active {
  transition: all 220ms ease;
}

.list-leave-active {
  position: absolute;
}

The list-move class animates the reorder: Vue applies it to elements that change position. Without a stable :key the group cannot match nodes and the movement never starts.

When CSS is enough and when it is not

CaseWinnerWhy
A modal appearingTransitionTwo classes and one opacity
Reordering a listTransitionGroupThe move class handles the shift
A timeline with several stepsA library or GSAPCSS does not orchestrate stateful sequences
Animation across a route changeView TransitionsThe browser keeps the page state

Common mistakes

  • Putting duration on enter-from: it is removed on the next frame and the animation jumps.
  • Animating height: auto: the browser cannot interpolate an automatic value, so use an explicit measure or grid-template-rows.
  • Forgetting appear: the first render has no transition and the modal pops in.
  • Using TransitionGroup without tag: the default wrapper is a span and breaks the list layout.

The limitation to plan for

CSS transitions do not honour the system preference for reduced motion on their own. Wrap the block in @media (prefers-reduced-motion: reduce) and zero the duration: whoever asked for less motion gets an instant change and keeps the content.

Where to go next

In the catalogue: vue3-gsap-animations, vue-bits and shadcn-vue; animated sections are in /blocks. The guides: animations with Tailwind and View Transitions. The comparison: transitions in React.

Frequently asked questions

Do I need a library to animate in Vue 3?

No. Transition and TransitionGroup with CSS cover entering, leaving and reordering. A library helps for complex timelines or stateful animation.

Why does the animation never start?

The selector usually misses the transition name. With name set to fade the classes are fade-enter-active and fade-leave-active.

Does Transition work with v-if?

Yes, that is the main case. It also works with v-show and dynamic components, not with a plain text change.

How do I honour prefers-reduced-motion?

Wrap the CSS block in a media query that zeroes the duration: whoever asked for less motion gets an instant change.

Why does the layout jump when the animation ends?

You are animating an element that takes space in flow. Animate transform and opacity, or remove the node from flow while it leaves.