pjax-max-general
v1.0.0
Published
Lightweight PJAX library for page transitions
Maintainers
Readme
pjax-max
A lightweight (~2.3KB gzipped), zero-dependency PJAX library for page transitions. Navigate between pages without full reloads, with full control over transition animations.
Built on top of the browser's native History API, Fetch API, and EventTarget.
Install
npm install pjax-max-generalimport { Core, Renderer, Transition } from 'pjax-max-general';Or via CDN:
<script src="https://unpkg.com/pjax-max-general"></script>
<script>
const { Core, Renderer, Transition } = PjaxMax;
</script>HTML Setup
pjax-max requires two data attributes in your markup:
<body>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<main data-router-wrapper>
<article data-router-view="home">
<!-- page content -->
</article>
</main>
</body>data-router-wrapper— The container that holds views. Stays in the DOM across navigations.data-router-view="slug"— The swappable view element. The slug maps to your renderers.
Quick Start
import { Core, Renderer, Transition } from 'pjax-max-general';
// 1. Define a transition
class Fade extends Transition {
out({ from, done }) {
from.style.opacity = 0;
setTimeout(() => {
from.remove();
done();
}, 300);
}
in({ to, done }) {
to.style.opacity = 0;
requestAnimationFrame(() => {
to.style.transition = 'opacity 0.3s';
to.style.opacity = 1;
setTimeout(done, 300);
});
}
}
// 2. Initialize
const core = new Core({
transitions: {
default: Fade
}
});That's it. All internal links will now use PJAX navigation with your fade transition.
Core
Constructor Options
const core = new Core({
renderers: { ... }, // Map of slug -> Renderer class
transitions: { ... }, // 'default' transition, contextual overrides, and optional _popstate
onLeave: [ ... ], // Functions called at the start of every out transition
onEnter: [ ... ], // Functions called at the start of every in transition
pageEntrance: { ... }, // Functions called after the in transition completes, keyed by namespace
});Properties
| Property | Type | Description |
|---|---|---|
| core.transitioning | boolean | true while a navigation is in progress |
| core.namespace | string | The current view's data-router-view slug |
| core.cache | Map | Page cache (URL -> properties) |
Methods
| Method | Description |
|---|---|
| core.redirect(url, contextual?, trigger?) | Navigate to a URL programmatically |
| core.attach(links) | Attach click listeners to links |
| core.detach(links) | Remove click listeners from links |
| core.prefetch(links) | Prefetch an array of links (elements or URL strings) into the cache |
Events
Listen to navigation lifecycle events using the native addEventListener API:
core.addEventListener('NAVIGATE_OUT', (e) => {
const { from, trigger, location } = e.detail;
});
core.addEventListener('NAVIGATE_IN', (e) => {
const { to, trigger, location } = e.detail;
});
core.addEventListener('NAVIGATE_END', (e) => {
const { from, to, trigger, location } = e.detail;
});Navigation Lifecycle Hooks
The onLeave and onEnter arrays let you run functions on every navigation without repeating logic in each transition. Common uses: closing menus, destroying scroll instances, resetting UI state.
const core = new Core({
transitions: { default: Fade },
onLeave: [
() => scroll?.destroy(),
() => megaNav.isOpen && megaNav.close(),
() => mobileMenu.isOpen && mobileMenu.close(),
],
onEnter: [
() => console.log('Entering:', core.namespace),
],
});onLeave runs before the out transition. onEnter runs after the new view is in the DOM but before the in transition plays. core.namespace is updated before onEnter fires, so it reflects the incoming page.
Page Entrance
The pageEntrance option lets you run functions after the in transition completes, keyed by namespace. This is useful for per-page entrance animations (e.g., staggering elements in with GSAP). A default key acts as a fallback for any namespace without a specific entry.
const core = new Core({
transitions: { default: Fade },
pageEntrance: {
default: ({ view }) => {
gsap.fromTo(view.querySelectorAll('[data-animate]'),
{ y: 20, opacity: 0 },
{ y: 0, opacity: 1, stagger: 0.1 }
);
},
home: ({ view }) => {
gsap.fromTo(view.querySelector('.hero'),
{ scale: 0.95, opacity: 0 },
{ scale: 1, opacity: 1, duration: 0.6 }
);
},
}
});Each function receives { view, from, trigger, location }:
| Property | Description |
|---|---|
| view | The incoming view element (data-router-view) |
| from | The previous view element, or null on initial page load |
| trigger | The link element that triggered navigation, 'popstate', 'script', or null on initial load |
| location | The current location object |
Page entrance also fires on the initial page load (after the initial renderer sets up), so your entrance animations run on first visit too.
Renderers
Renderers control page-specific setup and teardown. Extend the base Renderer class and override lifecycle hooks:
class HomeRenderer extends Renderer {
onEnter() {
// Called when the view is added to the DOM
}
onEnterCompleted() {
// Called after the in transition completes
}
onLeave() {
// Called before the out transition starts
}
onLeaveCompleted() {
// Called after the view is removed
}
}Map renderers to view slugs:
const core = new Core({
renderers: {
home: HomeRenderer,
about: AboutRenderer,
},
transitions: { default: Fade }
});Renderers support dynamic imports for code splitting:
const core = new Core({
renderers: {
home: () => import('./renderers/home.js'),
}
});Transitions
Transitions control the animation between views. Extend the base Transition class and implement in and out:
class Fade extends Transition {
out({ from, trigger, done }) {
// Animate the old view out, then call done()
gsap.to(from, {
opacity: 0,
duration: 0.4,
onComplete: () => {
from.remove();
done();
}
});
}
in({ to, from, trigger, done }) {
// Animate the new view in, then call done()
gsap.fromTo(to,
{ opacity: 0 },
{ opacity: 1, duration: 0.4, onComplete: done }
);
}
}Default vs. Contextual Transitions
A default transition applies to all navigations. Use contextual transitions on specific links with the data-transition attribute to override the default for that click:
const core = new Core({
transitions: {
default: Fade,
slideUp: SlideUp, // available as a contextual override
}
});<a href="/gallery" data-transition="slideUp">Gallery</a>When this link is clicked, SlideUp is used instead of Fade. If the named transition isn't registered in the transitions map, the default is used.
Keys starting with _ are reserved for the library (_popstate).
Priority (highest to lowest):
data-transitionattribute on the link (click navigation only)_popstatedirection transition (back/forward only)defaulttransition
Popstate Transitions (Back/Forward)
By default, browser back/forward navigations use the default transition. The _popstate reserved keyword lets you specify different transitions for each direction:
const core = new Core({
transitions: {
default: Fade,
_popstate: {
back: SlideRight,
forward: SlideLeft,
}
}
});Both back and forward are optional — specify one, both, or neither. If a direction is not specified, the default transition is used. If neither is specified, _popstate is ignored entirely.
pjax-max tracks history position internally to detect whether a popstate event is a back or forward navigation, since the browser does not expose this.
A priority field is also accepted on _popstate for backwards-compatibility, but it currently has no effect.
Overlapping Transitions
The old view stays in the DOM until you explicitly remove it. This means you can run both views simultaneously for overlapping transitions:
class CrossFade extends Transition {
out({ from, done }) {
gsap.to(from, { opacity: 0, duration: 0.6, onComplete: done });
// Don't call from.remove() here — let the in transition overlap
}
in({ to, from, trigger, done }) {
from.remove(); // Remove old view when ready
gsap.fromTo(to,
{ opacity: 0 },
{ opacity: 1, duration: 0.6, onComplete: done }
);
}
}Scroll Restoration
By default, pjax-max lets the browser handle scroll position naturally. If you want to always start at the top of the page after navigation, enable manualScrollRestoration:
const core = new Core({
transitions: { default: Fade },
manualScrollRestoration: true
});When enabled, this sets history.scrollRestoration = 'manual' and scrolls to the top instantly after the old view is removed and the new view is added to the DOM — before the in transition plays.
Defaults to false.
Event Mask
During transitions, user interactions like scrolling, clicking, or swiping can cause visual glitches. pjax-max automatically creates an invisible full-screen overlay that blocks these gestures while a transition is in progress.
The mask is appended to the <body> on initialization with pointer-events: none. When a navigation starts it switches to pointer-events: auto, capturing and preventing scroll, wheel, and touchmove events. When the transition completes it switches back to none.
No setup required — this is fully automatic.
Prefetching
Mark links with a class and pjax-max will prefetch the target page into core.cache. Subsequent navigation to that URL skips the network and runs the out transition against the cached properties immediately.
<a href="/about" class="prefetch-critical">About</a> <!-- fetched on page load -->
<a href="/work" class="prefetch-hover">Work</a> <!-- fetched on first hover -->.prefetch-critical— fetched immediately when the page initializes (and after every navigation, so prefetch links on freshly-loaded pages are picked up too)..prefetch-hover— fetched on the firstmouseenter. Only fires once per link.
Programmatic Prefetch
core.prefetch([linkElement, '/contact']); // accepts elements or URL stringsBehavior
- Same-origin only. Cross-origin URLs are skipped silently.
- Deduped. Already-cached or already-in-flight URLs are not re-fetched.
- Best-effort. Network errors are swallowed — prefetching never blocks or breaks navigation.
- Cache shared with navigation. Prefetched entries land in the same
core.cachethat navigation reads from.
Disabling PJAX on Links
Add data-router-disabled to opt out of PJAX navigation:
<a href="/external" data-router-disabled>Normal navigation</a>Links with a target attribute (e.g., target="_blank") are also ignored automatically. Cmd/Ctrl+click opens links in a new tab as expected.
Browser Support
All modern browsers. No IE support.
- Chrome / Edge
- Firefox
- Safari
