Реализация Page Transition анимаций (Barba.js) на сайте
Page transitions — анимированные переходы между страницами без полной перезагрузки браузера. Barba.js перехватывает навигацию, делает AJAX-запрос следующей страницы, анимирует выход текущей и вход новой. Для пользователя это выглядит как переход внутри приложения, а не перезагрузка.
Используется на multi-page сайтах (не SPA). В SPA-фреймворках (Next.js, Nuxt) — собственные механизмы роутинга с анимациями.
Установка и базовая структура
npm install @barba/core gsap
import barba from '@barba/core'
import gsap from 'gsap'
barba.init({
debug: false,
timeout: 5000,
transitions: [
{
name: 'default-transition',
// Вызывается до запроса следующей страницы
async leave(data) {
await gsap.to(data.current.container, {
opacity: 0,
y: -30,
duration: 0.4,
ease: 'power2.in',
})
},
// Вызывается после загрузки следующей страницы
async enter(data) {
gsap.from(data.next.container, {
opacity: 0,
y: 30,
duration: 0.5,
ease: 'power2.out',
})
},
},
],
})
HTML-разметка
Barba требует data-атрибуты для определения корневого контейнера и namespace страницы:
<!-- Каждая страница -->
<main data-barba="wrapper">
<div data-barba="container" data-barba-namespace="home">
<!-- Контент страницы -->
</div>
</main>
Namespace используется для routing — применять разные переходы для разных пар страниц.
Переходы с оверлеем
Более сложный паттерн: цветной overlay выезжает поверх страницы, затем уходит, открывая новый контент.
// DOM-элемент оверлея (всегда в DOM, вне data-barba container)
const overlay = document.querySelector('.transition-overlay')
barba.init({
transitions: [
{
name: 'overlay-transition',
async leave() {
// Оверлей въезжает снизу
await gsap.fromTo(overlay,
{ scaleY: 0, transformOrigin: 'bottom' },
{ scaleY: 1, duration: 0.5, ease: 'power3.inOut' }
)
},
async enter(data) {
// Новая страница уже готова — оверлей уходит вверх
await gsap.to(overlay, {
scaleY: 0,
transformOrigin: 'top',
duration: 0.5,
ease: 'power3.inOut',
})
// Анимация контента новой страницы
gsap.from(data.next.container.querySelectorAll('[data-animate-in]'), {
opacity: 0,
y: 40,
stagger: 0.08,
duration: 0.6,
ease: 'power2.out',
})
},
},
],
})
.transition-overlay {
position: fixed;
inset: 0;
background: #0a0a0a;
z-index: 9000;
transform: scaleY(0);
transform-origin: bottom;
pointer-events: none;
}
Routing — разные переходы для разных страниц
barba.init({
transitions: [
// Переход с главной на кейсы
{
name: 'home-to-work',
from: { namespace: ['home'] },
to: { namespace: ['work'] },
async leave(data) {
const title = data.current.container.querySelector('.hero-title')
await gsap.to(title, {
xPercent: -100,
opacity: 0,
duration: 0.6,
})
},
async enter(data) {
// ...
},
},
// Переход со страницы проекта назад
{
name: 'project-back',
from: { namespace: ['project'] },
async leave(data) {
// Клип-маска схлопывается до thumbnail
const hero = data.current.container.querySelector('.project-hero')
const targetRect = document.querySelector('.work-thumb.is-active')?.getBoundingClientRect()
if (targetRect) {
await gsap.to(hero, {
clipPath: `inset(${targetRect.top}px ${window.innerWidth - targetRect.right}px ${window.innerHeight - targetRect.bottom}px ${targetRect.left}px)`,
duration: 0.6,
ease: 'power3.inOut',
})
}
},
},
// Дефолтный — для всего остального
{
name: 'default',
async leave(data) {
await gsap.to(data.current.container, { opacity: 0, duration: 0.3 })
},
enter(data) {
gsap.from(data.next.container, { opacity: 0, duration: 0.3 })
},
},
],
})
Жизненный цикл и переинициализация
Главная сложность Barba: скрипты, которые инициализировали компоненты на странице, нужно запускать заново при каждом переходе.
// Инициализация компонентов страницы
function initPage(container: HTMLElement) {
// ScrollTrigger — обязательно обновить
ScrollTrigger.refresh()
// Lenis — сбросить позицию
lenis?.scrollTo(0, { immediate: true })
// Компоненты, привязанные к DOM
container.querySelectorAll('[data-slider]').forEach(initSlider)
container.querySelectorAll('[data-counter]').forEach(initCounter)
}
barba.hooks.after((data) => {
initPage(data.next.container)
})
// Убивать предыдущие инстансы перед уходом
barba.hooks.beforeLeave((data) => {
// Destroy ScrollTrigger instances привязанные к текущему контейнеру
ScrollTrigger.getAll()
.filter(st => data.current.container.contains(st.trigger as HTMLElement))
.forEach(st => st.kill())
})
Prefetch
Barba не загружает следующую страницу заранее из коробки. Для instant transitions — @barba/prefetch:
npm install @barba/prefetch
import barba from '@barba/core'
import barbaPrefetch from '@barba/prefetch'
barba.use(barbaPrefetch)
barba.init({ ... })
// Теперь страницы prefetch-ятся при hover на ссылках
SEO и аналитика
AJAX-переходы не вызывают стандартных page view событий. GA4 нужно триггерить вручную:
barba.hooks.after(({ next }) => {
// GA4
if (typeof gtag !== 'undefined') {
gtag('event', 'page_view', {
page_title: next.html.match(/<title>(.*?)<\/title>/)?.[1] || '',
page_location: window.location.href,
page_path: window.location.pathname,
})
}
// Обновление title и meta
const nextHead = new DOMParser()
.parseFromString(next.html, 'text/html')
.head
document.title = nextHead.querySelector('title')?.textContent || ''
// Meta description
const desc = nextHead.querySelector('meta[name="description"]')
if (desc) {
document.querySelector('meta[name="description"]')
?.setAttribute('content', desc.getAttribute('content') || '')
}
})
Сроки
Базовые переходы fade/slide для 2–3 типов страниц — 2–3 дня. Сложные морфинг-переходы с анимацией элементов, prefetch, интеграцией ScrollTrigger и Lenis, обновлением аналитики — 5–8 дней.







