ainer element and assign a namespace for context-aware routing.
<body>
<nav id="global-nav" aria-label="Primary navigation">
<!-- Persistent across transitions -->
</nav>
<div id="viewport-wrapper" data-barba-container="main" data-barba-namespace="default">
<!-- Server-rendered or static content replaces here -->
</div>
<footer id="global-footer">
<!-- Persistent across transitions -->
</footer>
</body>
The data-barba-container attribute marks the swap target. The data-barba-namespace enables route-specific lifecycle hooks. Global elements remain untouched, preserving event listeners, Web Audio contexts, and authenticated sessions.
Step 2: Transition Pipeline Architecture
Transitions must resolve asynchronously. Barba pauses the navigation queue until leave and enter hooks complete. We'll structure this using a modular pipeline that separates animation logic from navigation control.
import barba from '@barba/core';
import gsap from 'gsap';
interface TransitionPayload {
current: { container: HTMLElement };
next: { container: HTMLElement };
}
class NavigationPipeline {
private static readonly ANIMATION_DURATION = 450;
private static readonly EASE_PROFILE = 'power2.inOut';
public static async teardown(payload: TransitionPayload): Promise<void> {
const { current } = payload;
return gsap.to(current.container, {
opacity: 0,
y: -20,
duration: this.ANIMATION_DURATION / 1000,
ease: this.EASE_PROFILE,
onComplete: () => {
current.container.style.visibility = 'hidden';
}
});
}
public static async assemble(payload: TransitionPayload): Promise<void> {
const { next } = payload;
gsap.set(next.container, {
opacity: 0,
y: 20,
visibility: 'visible'
});
return gsap.to(next.container, {
opacity: 1,
y: 0,
duration: this.ANIMATION_DURATION / 1000,
ease: this.EASE_PROFILE
});
}
}
barba.init({
transitions: [
{
name: 'viewport-swap',
async leave(payload) {
await NavigationPipeline.teardown(payload);
},
async enter(payload) {
await NavigationPipeline.assemble(payload);
}
}
]
});
Architecture Rationale:
- Async/Await Enforcement: Barba's queue halts until promises resolve. Using
async/await prevents race conditions where enter animations trigger before leave completes.
- Visibility Toggling: Setting
visibility: hidden after opacity reaches zero prevents layout shifts and ensures the old container doesn't intercept pointer events during the swap.
- GSAP Integration: GSAP's ticker syncs with
requestAnimationFrame, guaranteeing 60fps performance. The library also handles vendor prefixes and GPU-accelerated transforms automatically.
- Modular Pipeline: Extracting animation logic into a class enables reuse across namespaces and simplifies testing.
Step 3: Context-Aware Routing
Different page types require distinct transition behaviors. Barba's views configuration allows namespace-specific lifecycle hooks without polluting the global transition queue.
barba.init({
views: [
{
namespace: 'project-detail',
beforeEnter(payload) {
const { next } = payload;
// Preload heavy assets specific to this route
next.container.dataset.preloaded = 'true';
},
afterEnter() {
// Initialize route-specific widgets
initializeMediaViewer();
syncAnalytics('project_view');
}
}
]
});
Namespaces decouple global navigation logic from page-specific initialization. This prevents unnecessary script execution and reduces memory pressure during rapid navigation.
Step 4: Lifecycle Synchronization
Third-party libraries (maps, carousels, analytics trackers) attach to DOM nodes. When Barba swaps containers, those nodes are destroyed. You must explicitly teardown and reinitialize dependent systems.
barba.hooks.after(() => {
// Reset scroll position to match browser expectations
window.scrollTo({ top: 0, behavior: 'instant' });
// Update document metadata
document.title = document.querySelector('h1')?.textContent || 'Default Title';
// Reinitialize idempotent services
AnalyticsTracker.pageView();
AccessibilityManager.announceRouteChange();
});
The after hook fires once the new container is mounted and animations complete. This is the correct placement for metadata updates, analytics pings, and focus management.
Pitfall Guide
1. State Leakage from Uncleaned Instances
Explanation: Event listeners, IntersectionObservers, and WebSockets attached to the old container persist in memory after DOM removal. This causes duplicate handlers, memory leaks, and erratic behavior during rapid navigation.
Fix: Implement a teardown registry. Attach cleanup functions to a Map keyed by container ID, and invoke them in the leave hook before DOM removal.
2. Animation Duration Inflation
Explanation: Durations exceeding 700ms create perceptible lag. Users interpret slow transitions as network latency, negating the performance benefits of client-side navigation.
Fix: Cap transitions at 400-550ms. Use hardware-accelerated properties (transform, opacity) exclusively. Avoid animating width, height, or margin which trigger layout recalculation.
3. Accessibility & Reduced Motion Blind Spots
Explanation: Forcing animations on users with vestibular disorders violates WCAG 2.2 guidelines. Screen readers also lose context if focus isn't explicitly managed post-transition.
Fix: Query window.matchMedia('(prefers-reduced-motion: reduce)') during initialization. If true, bypass GSAP timelines and instantly swap containers. Always call focus() on the new heading and update an aria-live="polite" region.
4. History API Desynchronization
Explanation: Barba updates the URL via pushState, but doesn't automatically sync scroll position, meta tags, or browser history state. This breaks the back button and confuses assistive technology.
Fix: In the after hook, explicitly call window.scrollTo(0, 0), update document.title, and sync meta tags. Use history.replaceState() if you need to adjust the current entry without adding to the stack.
5. Third-Party Script Duplication
Explanation: Analytics libraries, chat widgets, and ad scripts often execute on DOMContentLoaded. Running them again after every transition creates duplicate tracking events and UI glitches.
Fix: Wrap third-party initializations in idempotent guards. Check for existing instances before creating new ones. Use barba.hooks.after for single-execution services, and namespace-specific hooks for route-bound widgets.
6. Fetch Failure Cascades
Explanation: Network timeouts or 404 responses during Barba's internal fetch break the transition pipeline. The UI freezes, and the user is trapped in a half-loaded state.
Fix: Implement a fallback handler. If the fetch fails, call window.location.href = targetUrl to trigger a native reload. Log the error to your monitoring system and display a non-blocking toast notification.
7. Namespace Collision
Explanation: Assigning identical namespaces to unrelated routes causes hook conflicts. A beforeEnter hook meant for blog posts may incorrectly trigger on product pages.
Fix: Enforce strict namespace conventions. Use route prefixes (blog/post, shop/product) and validate namespace assignment during build time. Log warnings if multiple views match the same namespace.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Marketing / Portfolio Site | Barba-Enhanced MPA | Preserves SEO, enables cinematic transitions, minimal JS overhead | Low (CDN + GSAP) |
| SaaS Dashboard / Web App | Full Client SPA | Requires complex state management, real-time data, offline capability | High (Framework + Build Pipeline) |
| Documentation / Knowledge Base | Static Export + Barba | Fast initial load, context continuity, zero client routing complexity | Low (Static Host + Barba) |
| E-Commerce Catalog | Barba-Enhanced MPA | Maintains server-rendered product data, smooth category browsing | Medium (CDN + Asset Optimization) |
| Legacy WordPress Site | Barba-Enhanced MPA | No framework migration required, immediate UX uplift, theme-compatible | Low (Plugin/CDN) |
Configuration Template
// barba.config.ts
import barba from '@barba/core';
import gsap from 'gsap';
// Reduced motion guard
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
barba.init({
transitions: [
{
name: 'standard-swap',
async leave({ current }) {
if (prefersReducedMotion) return;
await gsap.to(current.container, {
opacity: 0,
duration: 0.4,
ease: 'power2.in'
});
},
async enter({ next }) {
if (prefersReducedMotion) {
next.container.style.opacity = '1';
return;
}
gsap.set(next.container, { opacity: 0 });
await gsap.to(next.container, {
opacity: 1,
duration: 0.45,
ease: 'power2.out'
});
}
}
],
hooks: {
after() {
window.scrollTo({ top: 0, behavior: 'instant' });
document.title = document.querySelector('h1')?.textContent || document.title;
// Reinitialize global services here
},
error() {
console.warn('[Barba] Navigation failed. Falling back to native reload.');
// Implement toast notification or retry logic
}
},
prefetch: true, // Preloads linked pages on hover/focus
prevent: ({ el }) => {
// Exclude external links, downloads, and anchors
return el.hasAttribute('data-barba-prevent') ||
el.getAttribute('target') === '_blank' ||
el.getAttribute('href')?.startsWith('#');
}
});
Quick Start Guide
- Install Dependencies: Run
npm install @barba/core gsap and import both modules in your entry point.
- Markup Setup: Wrap your dynamic content in
<div data-barba-container="main"> and assign data-barba-namespace per template.
- Initialize Pipeline: Call
barba.init() with a single transition object containing async leave and async enter hooks using GSAP timelines.
- Add Lifecycle Guards: Implement
barba.hooks.after for scroll reset, focus management, and metadata sync. Add prefers-reduced-motion checks to bypass animations.
- Deploy & Validate: Test navigation across 5+ pages, verify back/forward history behavior, audit memory usage in DevTools, and confirm SEO metadata updates correctly.