18. Переходы и анимации
🎬 Transitions и Animations в Vue 3
Заголовок раздела «🎬 Transitions и Animations в Vue 3»Vue имеет встроенную систему анимаций прямо из коробки. Не нужно ничего устанавливать для базовых переходов — просто оберни элемент в <Transition> и добавь CSS классы. Для продвинутых анимаций подключаем GSAP или vueuse/motion 🚀
Компонент <Transition>
Заголовок раздела «Компонент <Transition>»<Transition> — это встроенный Vue-компонент для анимации одного элемента при появлении/исчезновении:
<template> <button @click="show = !show">Показать/скрыть</button>
<Transition> <p v-if="show">Привет, анимация!</p> </Transition></template>
<style>/* Vue добавляет эти классы автоматически */
/* 1. Начало появления и конец исчезновения */.v-enter-from,.v-leave-to { opacity: 0;}
/* 2. Активная фаза переходов */.v-enter-active,.v-leave-active { transition: opacity 0.5s ease;}
/* 3. Конец появления и начало исчезновения */.v-enter-to,.v-leave-from { opacity: 1;}</style>Именованные переходы
Заголовок раздела «Именованные переходы»<Transition name="slide-fade"> <div v-if="show">Элемент</div></Transition>
<style>.slide-fade-enter-active { transition: all 0.3s ease-out;}
.slide-fade-leave-active { transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);}
.slide-fade-enter-from,.slide-fade-leave-to { transform: translateX(-20px); opacity: 0;}</style>Фазы transition — 6 CSS классов
Заголовок раздела «Фазы transition — 6 CSS классов»ПОЯВЛЕНИЕ (enter): enter-from → enter-active → enter-to (начало) (весь процесс) (конец)
ИСЧЕЗНОВЕНИЕ (leave): leave-from → leave-active → leave-to (начало) (весь процесс) (конец)/* Bounce эффект */.bounce-enter-active { animation: bounce-in 0.5s;}
.bounce-leave-active { animation: bounce-in 0.5s reverse;}
@keyframes bounce-in { 0% { transform: scale(0); } 50% { transform: scale(1.25); } 100% { transform: scale(1); }}mode=“out-in” и “in-out”
Заголовок раздела «mode=“out-in” и “in-out”»Когда один элемент заменяет другой, важно управлять порядком анимаций:
<!-- Без mode: оба анимируются одновременно (часто выглядит плохо) --><Transition> <span v-if="state === 'on'">ВКЛ</span> <span v-else>ВЫКЛ</span></Transition>
<!-- out-in: сначала уходит старый, потом появляется новый --><Transition name="fade" mode="out-in"> <component :is="currentComponent" :key="currentComponent" /></Transition>
<!-- in-out: сначала появляется новый, потом уходит старый --><Transition name="slide" mode="in-out"> <div :key="page">{{ page }}</div></Transition>JavaScript Hooks — анимации через JS
Заголовок раздела «JavaScript Hooks — анимации через JS»Для GSAP и других библиотек используй JavaScript хуки:
<script setup lang="ts">import { ref } from 'vue'import gsap from 'gsap'
function onBeforeEnter(el: HTMLElement) { el.style.opacity = '0' el.style.height = '0'}
function onEnter(el: HTMLElement, done: () => void) { gsap.to(el, { opacity: 1, height: 'auto', duration: 0.5, ease: 'power2.out', onComplete: done, // Обязательно вызвать done! })}
function onLeave(el: HTMLElement, done: () => void) { gsap.to(el, { opacity: 0, height: 0, duration: 0.4, ease: 'power2.in', onComplete: done, })}</script>
<template> <Transition :css="false" @before-enter="onBeforeEnter" @enter="onEnter" @leave="onLeave" > <div v-if="show">Анимированный контент</div> </Transition></template>Все хуки:
Заголовок раздела «Все хуки:»<Transition @before-enter="onBeforeEnter" <!-- До добавления в DOM --> @enter="onEnter" <!-- Начало появления --> @after-enter="onAfterEnter" <!-- Появление завершено --> @enter-cancelled="onEnterCancelled" <!-- Появление прервано -->
@before-leave="onBeforeLeave" <!-- До начала исчезновения --> @leave="onLeave" <!-- Начало исчезновения --> @after-leave="onAfterLeave" <!-- Исчезновение завершено --> @leave-cancelled="onLeaveCancelled">TransitionGroup — анимация списков
Заголовок раздела «TransitionGroup — анимация списков»<TransitionGroup> анимирует добавление, удаление и перемещение элементов списка:
<script setup lang="ts">import { ref } from 'vue'
const items = ref([1, 2, 3, 4, 5])
function add() { items.value.push(items.value.length + 1)}
function remove(id: number) { items.value = items.value.filter(i => i !== id)}
function shuffle() { items.value = [...items.value].sort(() => Math.random() - 0.5)}</script>
<template> <button @click="add">Добавить</button> <button @click="shuffle">Перемешать</button>
<!-- Важно: tag="ul" — указываем что рендерить как обёртку --> <!-- По умолчанию — Fragment (нет обёртки) --> <TransitionGroup name="list" tag="ul"> <li v-for="item in items" :key="item"> {{ item }} <button @click="remove(item)">✕</button> </li> </TransitionGroup></template>
<style>.list-enter-active,.list-leave-active { transition: all 0.5s ease;}
.list-enter-from,.list-leave-to { opacity: 0; transform: translateX(-30px);}
/* Магия! FLIP анимация при перемещении элементов */.list-move { transition: transform 0.5s ease;}
/* Пока элемент удаляется — выводим его из потока */.list-leave-active { position: absolute;}</style>GSAP интеграция
Заголовок раздела «GSAP интеграция»npm install gsap<!-- Staggered list с GSAP --><script setup lang="ts">import { ref } from 'vue'import gsap from 'gsap'
interface ListItem { id: number text: string}
function onBeforeEnter(el: HTMLElement) { el.style.opacity = '0' el.style.transform = 'translateY(-30px)'}
function onEnter(el: HTMLElement, done: () => void) { gsap.to(el, { opacity: 1, y: 0, duration: 0.4, // Stagger через data-index delay: Number(el.dataset.index) * 0.1, onComplete: done, })}
function onLeave(el: HTMLElement, done: () => void) { gsap.to(el, { opacity: 0, x: 100, duration: 0.3, delay: Number(el.dataset.index) * 0.05, onComplete: done, })}</script>
<template> <TransitionGroup :css="false" @before-enter="onBeforeEnter" @enter="onEnter" @leave="onLeave" > <div v-for="(item, index) in items" :key="item.id" :data-index="index" > {{ item.text }} </div> </TransitionGroup></template>Route Transitions — анимации роутера
Заголовок раздела «Route Transitions — анимации роутера»<script setup lang="ts">import { useRoute } from 'vue-router'
const route = useRoute()</script>
<template> <RouterView v-slot="{ Component, route: r }"> <Transition :name="r.meta.transition as string || 'fade'" mode="out-in" > <component :is="Component" :key="r.path" /> </Transition> </RouterView></template>
<style>/* Fade */.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease;}.fade-enter-from, .fade-leave-to { opacity: 0;}
/* Slide right */.slide-right-enter-active, .slide-right-leave-active { transition: transform 0.35s ease, opacity 0.35s ease;}.slide-right-enter-from { transform: translateX(-30px); opacity: 0;}.slide-right-leave-to { transform: translateX(30px); opacity: 0;}
/* Scale */.scale-enter-active, .scale-leave-active { transition: transform 0.3s ease, opacity 0.3s ease;}.scale-enter-from, .scale-leave-to { transform: scale(0.95); opacity: 0;}</style>@vueuse/motion — декларативные анимации
Заголовок раздела «@vueuse/motion — декларативные анимации»npm install @vueuse/motionimport { MotionPlugin } from '@vueuse/motion'
app.use(MotionPlugin)<!-- v-motion директива --><template> <!-- Базовая анимация появления --> <div v-motion :initial="{ opacity: 0, y: 100 }" :enter="{ opacity: 1, y: 0, transition: { duration: 500 } }" > Я появлюсь снизу! </div>
<!-- Hover анимация --> <button v-motion :initial="{ scale: 1 }" :hovered="{ scale: 1.05 }" :pressed="{ scale: 0.95 }" > Наведи на меня! </button>
<!-- Пресеты --> <div v-motion-fade>Fade появление</div> <div v-motion-slide-bottom>Появление снизу</div> <div v-motion-roll-visible-bottom>Появление при скролле</div> <div v-motion-pop>Pop эффект</div></template>Анимация числовых значений
Заголовок раздела «Анимация числовых значений»<script setup lang="ts">import { ref, watch } from 'vue'import { useTransition, TransitionPresets } from '@vueuse/core'
const targetValue = ref(0)
// Анимируем числоconst animatedValue = useTransition(targetValue, { duration: 800, transition: TransitionPresets.easeOutCubic,})</script>
<template> <input type="range" v-model="targetValue" min="0" max="100" /> <p>{{ Math.round(animatedValue) }}%</p></template>Шпаргалка
Заголовок раздела «Шпаргалка»<Transition> — один элемент, v-if/v-show<TransitionGroup> — список элементов, v-for
Классы:.name-enter-from — начало появления.name-enter-active — весь процесс появления.name-enter-to — конец появления.name-leave-from — начало исчезновения.name-leave-active — весь процесс исчезновения.name-leave-to — конец исчезновения.name-move — перемещение (TransitionGroup)
mode="out-in" — сначала старый уходит, потом новый приходитmode="in-in" — сначала новый приходит, потом старый уходит
:css="false" — отключить CSS классы, использовать только JS хукиappear — анимировать при первом рендере