Компоненты Transition и TransitionGroup, CSS-классы анимаций, JavaScript-хуки, GSAP и анимация списков
Vue предоставляет встроенную систему переходов, которая интегрирована с реактивностью и маршрутизацией. Правильно реализованные анимации улучшают UX — они дают пользователю понимание того, что происходит в интерфейсе.
CSS переходы — первый выбор в большинстве случаев. Браузер может оптимизировать их через GPU, они не блокируют главный поток, просты в написании и поддержке.
JavaScript анимации выбирайте когда:
<Transition>: базовое использование<Transition> оборачивает одиночный элемент или компонент и добавляет CSS-классы в нужные моменты жизненного цикла перехода:
<template>
<button @click="show = !show">Переключить</button>
<!-- name задаёт префикс CSS-классов -->
<Transition name="fade">
<p v-if="show" class="message">Привет! Я появляюсь с анимацией</p>
</Transition>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const show = ref(false)
</script>
<style scoped>
/* Появление */
.fade-enter-from { opacity: 0; }
.fade-enter-active { transition: opacity 0.3s ease; }
.fade-enter-to { opacity: 1; }
/* Исчезновение */
.fade-leave-from { opacity: 1; }
.fade-leave-active { transition: opacity 0.3s ease; }
.fade-leave-to { opacity: 0; }
</style>Шесть CSS-классов управляют всем переходом. Суффикс -active определяет саму анимацию (transition/animation), суффиксы -from/-to задают начальное и конечное состояние.
Типичная ошибка: писать opacity в -active вместо -from/-to. Класс -active присутствует на протяжении всего перехода, он задаёт timing-функцию, не значения.
По умолчанию входящий и исходящий элементы анимируются одновременно. Это часто выглядит как наложение. mode задаёт порядок:
<template>
<!-- out-in: сначала уходит старый, потом появляется новый -->
<Transition name="slide" mode="out-in">
<component :is="currentView" :key="currentView" />
</Transition>
<!-- in-in: сначала появляется новый, потом уходит старый -->
<Transition name="fade" mode="in-out">
<div v-if="isLoggedIn" key="user-panel">Панель пользователя</div>
<div v-else key="login-form">Форма входа</div>
</Transition>
</template>mode="out-in" — самый распространённый. Создаёт ощущение замены контента. Используйте его при переключении видов, вкладок, шагов форм.
Обратите внимание на :key — без него Vue может повторно использовать DOM-элемент и не запустить анимацию.
По умолчанию <Transition> не анимирует элемент при первом рендере страницы. Атрибут appear включает эту анимацию:
<template>
<Transition name="fade" appear>
<div class="hero-section">Добро пожаловать!</div>
</Transition>
</template>
<style>
/* Можно задать отдельные классы для первоначального появления */
.fade-appear-from { opacity: 0; transform: translateY(-20px); }
.fade-appear-active { transition: opacity 0.5s ease, transform 0.5s ease; }
.fade-appear-to { opacity: 1; transform: translateY(0); }
/* Если не заданы appear-классы, используются enter-классы */
</style>Хорошая практика для лендингов и презентационных страниц — добавляйте appear к hero-секциям и ключевым блокам.
<TransitionGroup> для анимации списков<TransitionGroup> анимирует добавление, удаление и перемещение элементов в списке. Он рендерит реальный DOM-элемент (по умолчанию <span>), что можно изменить через tag:
<template>
<div class="controls">
<button @click="addItem">Добавить</button>
<button @click="shuffle">Перемешать</button>
</div>
<!-- tag="ul" рендерит <ul> вместо дефолтного <span> -->
<TransitionGroup name="list" tag="ul" class="item-list">
<li v-for="item in items" :key="item.id" class="item">
{{ item.name }}
</li>
</TransitionGroup>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const items = ref([
{ id: 1, name: 'Первый' },
{ id: 2, name: 'Второй' },
{ id: 3, name: 'Третий' },
])
function addItem() {
const id = Date.now()
items.value.push({ id, name: `Элемент ${id}` })
}
function shuffle() {
items.value = [...items.value].sort(() => Math.random() - 0.5)
}
</script>
<style>
.list-enter-active,
.list-leave-active {
transition: all 0.4s ease;
}
.list-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* Ключевой класс для плавного перемещения существующих элементов */
.list-move {
transition: transform 0.4s ease;
}
/* При удалении нужно вывести элемент из потока, чтобы остальные плавно встали */
.list-leave-active {
position: absolute;
}
</style>Класс move-class (или автоматически ${name}-move) управляет анимацией перемещения. Без него элементы при перестановке прыгают мгновенно. Трюк с position: absolute во время leave-active необходим — иначе удаляемый элемент занимает место и остальные не могут плавно переместиться.
Когда CSS недостаточно, используйте JavaScript-хуки. Они дают полный контроль над каждым этапом:
<template>
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@before-leave="onBeforeLeave"
@leave="onLeave"
@after-leave="onAfterLeave"
:css="false"
>
<div v-if="show" class="panel">Контент</div>
</Transition>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const show = ref(false)
function onBeforeEnter(el: Element) {
// Установить начальное состояние до анимации
;(el as HTMLElement).style.opacity = '0'
;(el as HTMLElement).style.transform = 'scale(0.9)'
}
function onEnter(el: Element, done: () => void) {
// done() вызывается когда анимация завершена
const htmlEl = el as HTMLElement
htmlEl.animate(
[
{ opacity: 0, transform: 'scale(0.9)' },
{ opacity: 1, transform: 'scale(1)' }
],
{ duration: 300, easing: 'ease-out' }
).onfinish = done
}
function onLeave(el: Element, done: () => void) {
const htmlEl = el as HTMLElement
htmlEl.animate(
[
{ opacity: 1, transform: 'scale(1)' },
{ opacity: 0, transform: 'scale(0.9)' }
],
{ duration: 200, easing: 'ease-in' }
).onfinish = done
}
// Пустые хуки для TypeScript
function onAfterEnter(el: Element) {}
function onBeforeLeave(el: Element) {}
function onAfterLeave(el: Element) {}
</script>Атрибут :css="false" отключает автоматическое добавление CSS-классов — Vue не будет искать .v-enter-from и т.д. Используйте когда управляете анимацией полностью через JS.
Обязательно вызывайте done() в хуках enter и leave — иначе Vue не поймёт что переход завершён.
<KeepAlive> сохраняет состояние компонентов при переключении между ними. Без него каждое переключение — это полное уничтожение и создание заново:
<template>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab"
@click="activeTab = tab"
:class="{ active: activeTab === tab }"
>
{{ tab }}
</button>
</div>
<!-- KeepAlive кэширует компоненты между переключениями -->
<KeepAlive :include="['ProfileTab', 'SettingsTab']" :max="5">
<component :is="currentComponent" />
</KeepAlive>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import ProfileTab from './ProfileTab.vue'
import SettingsTab from './SettingsTab.vue'
import AnalyticsTab from './AnalyticsTab.vue'
const activeTab = ref('profile')
const tabs = ['profile', 'settings', 'analytics']
const components = {
profile: ProfileTab,
settings: SettingsTab,
analytics: AnalyticsTab,
}
const currentComponent = computed(() => components[activeTab.value as keyof typeof components])
</script>Параметры include и exclude фильтруют кэшируемые компоненты по имени (совпадает с name в defineOptions или названием файла). max ограничивает размер кэша — при превышении наименее используемый компонент уничтожается.
Кэшированные компоненты получают специальные lifecycle хуки:
<script setup lang="ts">
import { onActivated, onDeactivated } from 'vue'
// Вызывается когда компонент становится активным (не только при монтировании)
onActivated(() => {
console.log('Вкладка стала активной — обновим данные')
refreshData()
})
// Вызывается когда компонент скрывается (не уничтожается!)
onDeactivated(() => {
console.log('Вкладка скрыта — остановим polling')
stopPolling()
})
</script>GSAP (GreenSock Animation Platform) — самая мощная библиотека JS-анимаций. Интегрируется с Vue через JavaScript-хуки:
npm install gsap<template>
<Transition
@enter="gsapEnter"
@leave="gsapLeave"
:css="false"
>
<div v-if="show" ref="cardRef" class="card">
<h2>Карточка с GSAP анимацией</h2>
<p>Плавное появление с эффектом</p>
</div>
</Transition>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { gsap } from 'gsap'
const show = ref(false)
function gsapEnter(el: Element, done: () => void) {
gsap.from(el, {
duration: 0.5,
opacity: 0,
y: -50,
scale: 0.95,
ease: 'power2.out',
onComplete: done
})
}
function gsapLeave(el: Element, done: () => void) {
gsap.to(el, {
duration: 0.3,
opacity: 0,
y: 50,
scale: 0.95,
ease: 'power2.in',
onComplete: done
})
}
</script>GSAP Timeline позволяет создавать сложные последовательности:
function gsapEnter(el: Element, done: () => void) {
const tl = gsap.timeline({ onComplete: done })
tl.from(el, { opacity: 0, duration: 0.3 })
.from(el.querySelector('h2'), { y: -20, opacity: 0, duration: 0.2 }, '-=0.1')
.from(el.querySelector('p'), { y: 20, opacity: 0, duration: 0.2 }, '-=0.1')
}Для анимации переходов между страницами используйте слот <RouterView> с <Transition>:
<!-- App.vue или Layout.vue -->
<template>
<RouterView v-slot="{ Component, route }">
<Transition
:name="route.meta.transition as string || 'fade'"
mode="out-in"
>
<!-- :key обеспечивает что при смене маршрута компонент пересоздаётся -->
<component :is="Component" :key="route.path" />
</Transition>
</RouterView>
</template>В метаданных маршрутов задайте тип анимации:
const routes = [
{
path: '/dashboard',
component: Dashboard,
meta: { transition: 'slide-left' }
},
{
path: '/settings',
component: Settings,
meta: { transition: 'slide-right' }
}
]Vue 3 позволяет использовать реактивные значения прямо в CSS через v-bind():
<script setup lang="ts">
import { ref } from 'vue'
const duration = ref(300) // миллисекунды
const primaryColor = ref('#3b82f6')
</script>
<template>
<div class="animated-box">Анимированный блок</div>
</template>
<style scoped>
.animated-box {
/* v-bind() использует реактивное значение из setup */
transition: all v-bind(duration + 'ms') ease;
background-color: v-bind(primaryColor);
}
</style>Это особенно полезно для компонентов с настраиваемыми анимациями через props.
Анимируйте только transform и opacity — они не вызывают layout reflow и обрабатываются GPU:
/* Хорошо — только transform и opacity */
.card-enter-from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
/* Плохо — width и height вызывают reflow на каждом кадре */
.card-enter-from {
width: 0;
height: 0;
}Подсказка браузеру об интенсивной анимации. will-change нужно использовать экономно — только для элементов, которые гарантированно анимируются:
.animated-element {
will-change: transform, opacity;
}Добавляйте will-change через JavaScript непосредственно перед анимацией и убирайте после — это правильнее, чем держать его постоянно.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.