Transition Functions

This is where your animations live! We'll break down the three transition functions: once, leave, and enter. If using one of our templates, it's this part of the boilerplate that you will replace with the template code.
Lesson Notes
Overview
The boilerplate has three transition functions:
runPageOnceAnimation()first page load, no previous pagerunPageLeaveAnimation()old page exitsrunPageEnterAnimation()new page enters
Once animation
Runs on the very first page load. There's no leave animation because there's no previous page. Use this for intro sequences, loaders, or a special first-visit animation.
Leave animation
The old page is exiting. You get access to current (the container leaving) and next (the container coming in). This can be as simple as a fade out, or as complex as you want; Elements staggering off, overlays wiping across, whatever fits your transition. We remove the old container from the DOM when the animation completes.
Enter animation
The new page is coming in. Same deal; This can be simple or elaborate depending on what you're building. We animate the container, mark a pageReady moment, call resetPage(), and return a Promise so Barba knows when we're done.
Because we're in sync mode, the enter timeline starts at the same moment as leave. So to control exactly when our enter animation begins, we add a startEnter label at a specific time (in this case, 0.6 seconds). Then we attach our tweens to that label.
function runPageEnterAnimation(next){
const tl = gsap.timeline();
tl.add("startEnter", 0.6);
tl.fromTo(next, {
autoAlpha: 0,
},{
autoAlpha: 1,
}, "startEnter");
tl.add("pageReady");
tl.call(resetPage, [next], "pageReady");
return new Promise(resolve => {
tl.call(resolve, null, "pageReady");
});
}The startEnter label
Because leave and enter run simultaneously in sync mode, we use startEnter to control exactly when our enter animation begins. In this example, the enter animation waits 0.6 seconds before starting, giving the leave animation time to do its thing. So you will have to test and tweak this a little, depending on the animation you're working on.
The pageReady label
This marks the moment the page is usable: scroll works, interactions work, everything's settled. We call resetPage() here which scrolls to top, clears the fixed positioning from the transition, and restarts Lenis.
Why return a Promise?
The Promise tells Barba when the transition is "done." By resolving at pageReady, detail animations can continue after (text fading in, elements staggering) without blocking the lifecycle. The page becomes interactive while the extra polish happens in the background.
Reduced motion
If the user prefers reduced motion, we skip the animation entirely. Set the final state, resolve the Promise, done. It will basically make the site behave like a 'regular' non-BarbaJS powered website.
Boilerplate
// -----------------------------------------
// OSMO PAGE TRANSITION BOILERPLATE
// -----------------------------------------
gsap.registerPlugin(CustomEase);
history.scrollRestoration = "manual";
let lenis = null;
let nextPage = document;
let onceFunctionsInitialized = false;
const hasLenis = typeof window.Lenis !== "undefined";
const hasScrollTrigger = typeof window.ScrollTrigger !== "undefined";
const rmMQ = window.matchMedia("(prefers-reduced-motion: reduce)");
let reducedMotion = rmMQ.matches;
rmMQ.addEventListener?.("change", e => (reducedMotion = e.matches));
rmMQ.addListener?.(e => (reducedMotion = e.matches));
const has = (s) => !!nextPage.querySelector(s);
let staggerDefault = 0.05;
let durationDefault = 0.6;
CustomEase.create("osmo", "0.625, 0.05, 0, 1");
gsap.defaults({ ease: "osmo", duration: durationDefault });
// -----------------------------------------
// FUNCTION REGISTRY
// -----------------------------------------
function initOnceFunctions() {
initLenis();
if (onceFunctionsInitialized) return;
onceFunctionsInitialized = true;
// Runs once on first load
// if (has('[data-something]')) initSomething();
}
function initBeforeEnterFunctions(next) {
nextPage = next || document;
// Runs before the enter animation
// if (has('[data-something]')) initSomething();
}
function initAfterEnterFunctions(next) {
nextPage = next || document;
// Runs after enter animation completes
// if (has('[data-something]')) initSomething();
if(hasLenis){
lenis.resize();
}
if (hasScrollTrigger) {
ScrollTrigger.refresh();
}
}
// -----------------------------------------
// PAGE TRANSITIONS
// -----------------------------------------
function runPageOnceAnimation(next) {
const tl = gsap.timeline();
tl.call(() => {
resetPage(next)
}, null, 0);
return tl;
}
function runPageLeaveAnimation(current, next) {
const tl = gsap.timeline({
onComplete: () => { current.remove() }
});
if (reducedMotion) {
// Immediate swap behavior if user prefers reduced motion
return tl.set(current, { autoAlpha: 0 });
}
tl.to(current, { autoAlpha: 0, duration: 0.4 });
return tl;
}
function runPageEnterAnimation(next){
const tl = gsap.timeline();
if (reducedMotion) {
// Immediate swap behavior if user prefers reduced motion
tl.set(next, { autoAlpha: 1 });
tl.add("pageReady")
tl.call(resetPage, [next], "pageReady");
return new Promise(resolve => tl.call(resolve, null, "pageReady"));
}
tl.add("startEnter", 0.6);
tl.fromTo(next, {
autoAlpha: 0,
},{
autoAlpha: 1,
}, "startEnter");
tl.add("pageReady");
tl.call(resetPage, [next], "pageReady");
return new Promise(resolve => {
tl.call(resolve, null, "pageReady");
});
}
// -----------------------------------------
// BARBA HOOKS + INIT
// -----------------------------------------
barba.hooks.beforeEnter(data => {
// Position new container on top
gsap.set(data.next.container, {
position: "fixed",
top: 0,
left: 0,
right: 0,
});
if(hasLenis){
lenis.stop();
}
initBeforeEnterFunctions(data.next.container);
applyThemeFrom(data.next.container);
});
barba.hooks.afterLeave(() => {
if(hasScrollTrigger){
ScrollTrigger.getAll().forEach(trigger => trigger.kill());
}
});
barba.hooks.enter(data => {
initBarbaNavUpdate(data);
})
barba.hooks.afterEnter(data => {
// Run page functions
initAfterEnterFunctions(data.next.container);
// Settle
if(hasLenis){
lenis.resize();
lenis.start();
}
if(hasScrollTrigger){
ScrollTrigger.refresh();
}
});
barba.init({
debug: true, // Set to 'false' in production
timeout: 7000,
preventRunning: true,
transitions: [
{
name: "default",
sync: true,
// First load
async once(data) {
initOnceFunctions();
return runPageOnceAnimation(data.next.container);
},
// Current page leaves
async leave(data) {
return runPageLeaveAnimation(data.current.container, data.next.container);
},
// New page enters
async enter(data) {
return runPageEnterAnimation(data.next.container);
}
}
],
});
// -----------------------------------------
// GENERIC + HELPERS
// -----------------------------------------
const themeConfig = {
light: {
nav: "dark",
transition: "light"
},
dark: {
nav: "light",
transition: "dark"
}
};
function applyThemeFrom(container) {
const pageTheme = container?.dataset?.pageTheme || "light";
const config = themeConfig[pageTheme] || themeConfig.light;
document.body.dataset.pageTheme = pageTheme;
const transitionEl = document.querySelector('[data-theme-transition]');
if (transitionEl) {
transitionEl.dataset.themeTransition = config.transition;
}
const nav = document.querySelector('[data-theme-nav]');
if (nav) {
nav.dataset.themeNav = config.nav;
}
}
function initLenis() {
if (lenis) return; // already created
if (!hasLenis) return;
lenis = new Lenis({
lerp: 0.165,
wheelMultiplier: 1.25,
});
if (hasScrollTrigger) {
lenis.on("scroll", ScrollTrigger.update);
}
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
}
function resetPage(container){
window.scrollTo(0, 0);
gsap.set(container, { clearProps: "position,top,left,right" });
if(hasLenis){
lenis.resize();
lenis.start();
}
}
function debounceOnWidthChange(fn, ms) {
let last = innerWidth,
timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
if (innerWidth !== last) {
last = innerWidth;
fn.apply(this, args);
}
}, ms);
};
}
function initBarbaNavUpdate(data) {
var tpl = document.createElement('template');
tpl.innerHTML = data.next.html.trim();
var nextNodes = tpl.content.querySelectorAll('[data-barba-update]');
var currentNodes = document.querySelectorAll('nav [data-barba-update]');
currentNodes.forEach(function (curr, index) {
var next = nextNodes[index];
if (!next) return;
// Aria-current sync
var newStatus = next.getAttribute('aria-current');
if (newStatus !== null) {
curr.setAttribute('aria-current', newStatus);
} else {
curr.removeAttribute('aria-current');
}
// Class list sync
var newClassList = next.getAttribute('class') || '';
curr.setAttribute('class', newClassList);
});
}
// -----------------------------------------
// YOUR FUNCTIONS GO BELOW HERE
// -----------------------------------------




















































































































































