Page Name Transition (Wipe)
A Barba.js page transition where a solid panel wipes up to cover the screen, displays the name of the destination page, then continues upward to reveal the new page underneath. The page name is read from a data-page-name attribute on the Barba container. The current page shifts slightly upward as the panel covers it; the new page rises from below on reveal.
Setup — External Scripts
<!-- CSS -->
<link rel="stylesheet" href="https://unpkg.com/lenis@1.3.17/dist/lenis.css">
<!-- JS -->
<script src="https://cdn.jsdelivr.net/npm/@barba/core@2.10.3/dist/barba.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lenis@1.3.17/dist/lenis.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/CustomEase.min.js"></script><body data-barba="wrapper">
<main data-barba="container">
<section></section>
<section></section>
<section></section>
</main>
</body>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 initOnceFunctions() {
initLenis();
if (onceFunctionsInitialized) return;
onceFunctionsInitialized = true;
}
function initBeforeEnterFunctions(next) {
nextPage = next || document;
}
function initAfterEnterFunctions(next) {
nextPage = next || document;
if(hasLenis) lenis.resize();
if (hasScrollTrigger) ScrollTrigger.refresh();
}
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) 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) {
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.beforeEnter(data => {
gsap.set(data.next.container, { position: "fixed", top: 0, left: 0, right: 0 });
if (lenis && typeof lenis.stop === "function") 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 => {
initAfterEnterFunctions(data.next.container);
if(hasLenis){ lenis.resize(); lenis.start(); }
if(hasScrollTrigger) ScrollTrigger.refresh();
});
barba.init({
debug: true,
timeout: 7000,
preventRunning: true,
transitions: [{
name: "default",
sync: true,
async once(data) { initOnceFunctions(); return runPageOnceAnimation(data.next.container); },
async leave(data) { return runPageLeaveAnimation(data.current.container, data.next.container); },
async enter(data) { return runPageEnterAnimation(data.next.container); }
}],
});
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 || !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;
var newStatus = next.getAttribute('aria-current');
if (newStatus !== null) curr.setAttribute('aria-current', newStatus);
else curr.removeAttribute('aria-current');
curr.setAttribute('class', next.getAttribute('class') || '');
});
}Code
<div data-transition-wrap class="transition">
<div data-transition-panel class="transition__panel">
<span data-transition-label class="transition__label">
<span>[ </span>
<span data-transition-label-text>Welcome</span>
<span> ]</span>
</span>
</div>
</div>.transition {
z-index: 100;
pointer-events: none;
position: fixed;
inset: 0;
overflow: clip;
}
.transition__panel {
background-color: #30463e;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
display: flex;
position: absolute;
top: 100%;
left: 0;
}
.transition__label {
color: #cbe88a;
text-transform: uppercase;
font-family: Haffer Mono, Arial, sans-serif;
font-size: 2.5em;
}function runPageOnceAnimation(next) {
const tl = gsap.timeline();
tl.call(() => {
resetPage(next);
}, null, 0);
return tl;
}
function runPageLeaveAnimation(current, next) {
const transitionWrap = document.querySelector("[data-transition-wrap]");
const transitionPanel = transitionWrap.querySelector("[data-transition-panel]");
const transitionLabel = transitionWrap.querySelector("[data-transition-label]");
const transitionLabelText = transitionWrap.querySelector("[data-transition-label-text]");
const nextPageName = next.getAttribute("data-page-name")
transitionLabelText.innerText = nextPageName || "Hi there";
const tl = gsap.timeline({
onComplete: () => { current.remove() }
});
if (reducedMotion) {
// Immediate swap behavior if user prefers reduced motion
return tl.set(current, { autoAlpha: 0 });
}
tl.set(transitionPanel, {
autoAlpha: 1
}, 0);
tl.set(next,{
autoAlpha: 0
}, 0);
tl.fromTo(transitionPanel,{
yPercent: 0
},{
yPercent: -100,
duration: 0.8,
}, 0);
tl.fromTo(transitionLabel, {
autoAlpha: 0
},{
autoAlpha: 1
}, "<+=0.2");
tl.fromTo(current,{
y: "0vh"
},{
y: "-15vh",
duration: 0.8,
}, 0);
}
function runPageEnterAnimation(next){
const transitionWrap = document.querySelector("[data-transition-wrap]");
const transitionPanel = transitionWrap.querySelector("[data-transition-panel]");
const transitionLabel = transitionWrap.querySelector("[data-transition-label]");
const transitionLabelText = transitionWrap.querySelector("[data-transition-label-text]");
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", 1.25);
tl.set(next, {
autoAlpha: 1,
}, "startEnter");
tl.fromTo(transitionPanel, {
yPercent: -100,
},{
yPercent: -200,
duration: 1,
overwrite: "auto",
immediateRender: false
}, "startEnter");
tl.set(transitionPanel, {
autoAlpha: 0
}, ">");
tl.fromTo(transitionLabel, {
autoAlpha: 1
},{
autoAlpha: 0,
duration: 0.4,
overwrite: "auto",
immediateRender: false
}, "startEnter+=0.1");
tl.from(next, {
y: "15vh",
duration: 1,
}, "startEnter");
tl.add("pageReady");
tl.call(resetPage, [next], "pageReady");
return new Promise(resolve => {
tl.call(resolve, null, "pageReady");
});
}Notes
- •Requires Barba.js, GSAP, CustomEase, and Lenis loaded via CDN before the script runs.
- •The transition div must be placed outside the Barba container so it persists across page changes.
- •Add data-page-name on every Barba container — the transition reads the attribute from the incoming container.
- •reducedMotion support is built in — users with prefers-reduced-motion get an immediate swap with no animation.
- •The panel's autoAlpha is reset to 0 at the end of the enter animation so it is invisible and ready for the next transition.
Guide
Template Setup
Navigation lives inside the Barba container. The transition div sits outside. Add data-page-name on every container.
<body data-barba="wrapper">
<div data-transition-wrap>...</div>
<main data-barba="container" data-page-name="Homepage">
<nav>...</nav>
<!-- page content here -->
</main>
</body>Page name attribute
Add data-page-name="name here" on every Barba container on your website. The transition reads this value and sets it as the label text. If the attribute is missing the fallback text "Hi there" is used.
How to use
Copy the full JavaScript code block from this page and replace the // PAGE TRANSITIONS section in your boilerplate. This includes the transition functions.
Transition explained
The panel starts below the viewport (top: 100%) and wipes upward to cover the screen at yPercent: -100. The current page shifts up by 15vh as the panel covers it, creating depth. The label fades in while the panel is covering the screen, giving the page name time to be read. At startEnter (1.25s), the next page is revealed and the panel continues to yPercent: -200 to exit above the viewport. The new page rises from y: 15vh as it appears.