Реалізація 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-атрибути для визначення корневого контейнера та простору імен сторінки:
<!-- Кожна сторінка -->
<main data-barba="wrapper">
<div data-barba="container" data-barba-namespace="home">
<!-- Вміст сторінки -->
</div>
</main>
Простір імен використовується для рутингу — застосовувати різні переходи для різних пар сторінок.
Переходи з оверлеєм
Більш складний паттерн: кольоровий overlay їде поверх сторінки, потім йде, відкриваючи новий вміст.
// DOM-елемент оверлея (завжди в DOM, поза data-barba контейнером)
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;
}
Рутинг — Різні переходи для різних сторінок
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) {
// Клип-маска схлопується до мініатюри
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 не завантажує наступну сторінку заздалегідь з коробки. Для миттєвих переходів — @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 дні. Складні morphing-переходи з анімацією елементів, prefetch, інтеграцією ScrollTrigger та Lenis, оновленням аналітики — 5–8 днів.







