Zum Inhalt springen

Animationen und Übergänge

Wenn ein Element erscheint, verschwindet oder seinen Platz wechselt, springt die Seite ohne Animation von einem Zustand in den nächsten. Ein kurzer Übergang zeigt, was sich geändert hat. Vue bringt dafür zwei Komponenten mit, <Transition> für ein einzelnes Element und <TransitionGroup> für Listen. Beide setzen zu den richtigen Zeitpunkten CSS-Klassen, die eigentliche Animation schreibst du mit CSS.

Übergänge lohnen sich dort, wo sonst etwas unbemerkt passiert, etwa wenn eine Meldung erscheint, ein Eintrag aus einer Liste verschwindet oder ein Dialog aufgeht. Halte sie kurz, wenige Zehntelsekunden genügen. Manche Menschen reagieren auf Bewegung mit Schwindel oder Übelkeit und stellen im Betriebssystem ein, dass sie weniger Bewegung wollen. CSS fragt diese Einstellung mit der Media Query prefers-reduced-motion ab, und jede Animation in deinem Projekt sollte sie beachten.

<Transition> umschliesst ein Element, das mit v-if oder v-show erscheint und verschwindet. Beim Erscheinen setzt Vue nacheinander die Klassen fade-enter-from, fade-enter-active und fade-enter-to, beim Verschwinden die entsprechenden Klassen mit leave. Der Teil fade stammt aus dem Attribut name. Vue wartet, bis die Transition im CSS fertig ist, und entfernt das Element erst danach.

App.vue
<script setup>
import { ref } from 'vue'
const sent = ref(false)
</script>
<template>
<button type="button" @click="sent = !sent">
{{ sent ? 'Meldung schliessen' : 'Anfrage senden' }}
</button>
<!-- Transition adds classes while the paragraph enters and leaves -->
<Transition name="fade">
<p v-if="sent" class="notice">Deine Anfrage ist bei Lorraine Commons angekommen.</p>
</Transition>
</template>
<style>
.notice {
padding: 0.5rem 0.75rem;
border-left: 4px solid #2e7d32;
background: #e8f5e9;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.4s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* Whoever asks the system for less motion sees the change without the fade */
@media (prefers-reduced-motion: reduce) {
.fade-enter-active,
.fade-leave-active {
transition: none;
}
}
</style>

Für Listen nimmt <TransitionGroup> jeden Eintrag einzeln. Jeder braucht einen Schlüssel, damit Vue weiss, welcher Eintrag kommt, geht oder sich bewegt. Die Klasse list-move animiert die Einträge, die ihren Platz ändern. Damit die anderen Einträge nachrücken können, während einer verschwindet, nimmt position: absolute den verschwindenden Eintrag aus dem Fluss der Seite.

App.vue

App.vue
<script setup>
import { ref } from 'vue'
import { things } from './things.js'
const favorites = ref(things.slice(0, 3))
function add(thing) {
if (!favorites.value.some((favorite) => favorite.id === thing.id)) favorites.value = [...favorites.value, thing]
}
function remove(id) {
favorites.value = favorites.value.filter((favorite) => favorite.id !== id)
}
function sortByName() {
favorites.value = favorites.value.toSorted((a, b) => a.name.localeCompare(b.name))
}
</script>
<template>
<p class="actions">
<button v-for="thing in things" :key="thing.id" type="button" @click="add(thing)">{{ thing.name }} merken</button>
<button type="button" @click="sortByName">Nach Name sortieren</button>
</p>
<!-- TransitionGroup animates entries that enter, leave and move, each needs its key -->
<TransitionGroup name="list" tag="ul" class="favorites">
<li v-for="thing in favorites" :key="thing.id">
{{ thing.name }}
<button type="button" @click="remove(thing.id)">Entfernen</button>
</li>
</TransitionGroup>
</template>
<style>
.actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.favorites {
position: relative;
padding: 0;
list-style: none;
}
.favorites li {
display: flex;
justify-content: space-between;
gap: 1rem;
width: 100%;
padding: 0.25rem 0;
}
.list-move,
.list-enter-active,
.list-leave-active {
transition: all 0.4s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(2rem);
}
/* A leaving entry leaves the flow, so the others can move up while it fades */
.list-leave-active {
position: absolute;
}
@media (prefers-reduced-motion: reduce) {
.list-move,
.list-enter-active,
.list-leave-active {
transition: none;
}
}
</style>

things.js

things.js
// A few things of the catalog
export const things = [
{ id: 1, name: 'Bohrmaschine', category: 'Werkzeug', deposit: 20, availableFrom: null },
{ id: 2, name: 'Zelt für vier Personen', category: 'Camping', deposit: 50, availableFrom: '2026-11-12' },
{ id: 5, name: 'Stichsäge', category: 'Werkzeug', deposit: 20, availableFrom: null },
{ id: 6, name: 'Beamer', category: 'Feste', deposit: 80, availableFrom: null },
{ id: 15, name: 'Kreissäge', category: 'Werkzeug', deposit: 40, availableFrom: '2026-11-05' },
]

Zwischen Seiten animiert <Transition> die Komponente, die RouterView über seinen Slot herausgibt. Mit mode="out-in" verschwindet die alte Seite, bevor die neue erscheint.

App.vue

App.vue
<script setup>
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<nav>
<RouterLink to="/">Katalog</RouterLink>
<RouterLink to="/association">Verein</RouterLink>
</nav>
<!-- RouterView hands over the page, Transition animates the change, out-in lets the old page leave first -->
<RouterView v-slot="{ Component }">
<Transition name="page" mode="out-in">
<component :is="Component" />
</Transition>
</RouterView>
</template>
<style>
nav {
display: flex;
gap: 1rem;
}
.page-enter-active,
.page-leave-active {
transition:
opacity 0.25s ease,
transform 0.25s ease;
}
.page-enter-from {
opacity: 0;
transform: translateY(0.5rem);
}
.page-leave-to {
opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
.page-enter-active,
.page-leave-active {
transition: none;
}
}
</style>

AssociationView.vue

AssociationView.vue
<template>
<section>
<h2>Verein</h2>
<p>Offen am Mittwoch von 17 bis 19 Uhr und am Samstag von 10 bis 13 Uhr.</p>
</section>
</template>

CatalogView.vue

CatalogView.vue
<template>
<section>
<h2>Katalog</h2>
<p>Rund 60 Dinge in sechs Kategorien.</p>
</section>
</template>

main.js

main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router.js'
createApp(App).use(router).mount('#app')

router.js

router.js
import { createMemoryHistory, createRouter } from 'vue-router'
import AssociationView from './AssociationView.vue'
import CatalogView from './CatalogView.vue'
export default createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', component: CatalogView },
{ path: '/association', component: AssociationView },
],
})

In allen drei Beispielen schaltet eine Media Query die Übergänge ab, wenn das System weniger Bewegung verlangt. Die Änderung selbst bleibt, sie geschieht nur ohne Animation.

Browser bringen inzwischen eine eigene Schnittstelle für solche Übergänge mit. document.startViewTransition() hält ein Bild der Seite fest, lässt dich den Zustand ändern und blendet vom alten zum neuen Bild über. Die Methode gibt es in Chrome und Edge ab Version 111, in Safari ab 18 und in Firefox ab 144. Prüf trotzdem, ob es sie gibt, und ändere den Zustand sonst direkt.

Texte und Abbildungen stehen unter CC BY-SA 4.0, der Code unter der MIT-Lizenz und die Daten des Kurses unter CC0 1.0. Die Schrift der Abbildungen ist Kalam unter der SIL Open Font License 1.1. Die Quellen liegen auf GitHub.