npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pjax-max

v4.0.0

Published

Lightweight PJAX library for page transitions

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
import { Core, Renderer, Transition } from 'pjax-max';

Or via CDN:

<script src="https://unpkg.com/pjax-max"></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';

// 1. Define a transition
class Fade extends Transition {
  out({ from, done }) {
    gsap.to(from, {
      opacity: 0,
      duration: 0.3,
      force3D: true,
      ease: 'sine.out',
      onComplete: () => {
        from.remove();
        done();
      }
    });
  }

  in({ to, done }) {
    gsap.fromTo(to,
      { opacity: 0 },
      { opacity: 1, duration: 0.3, force3D: true, ease: 'sine.out', onComplete: () => { done } }
    );
  }
}

// 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.precache(url, html) | Store pre-fetched HTML in the cache without triggering a fetch |

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. Pathname vs. Contextual Transitions

A default transition applies to all navigations. Pathname-based transitions let you assign transitions to specific destination pages by matching on the URL pathname:

const core = new Core({
  transitions: {
    default: Fade,
    '_home': FadeIn,               // used when navigating to /
    'about': SlideUp,              // used when navigating to /about
    'work': CrossFade,             // used when navigating to /work
    'work/project': ZoomIn,        // used when navigating to /work/project
    slideUp: SlideUp,              // available as a contextual override
  }
});

The pathname is cleaned of leading/trailing slashes before lookup (e.g., /about/about). The root path / maps to _home. The match is exact — /work/project-name will not fall back to work.

Keys starting with _ are reserved for the library (_home, _popstate).

Use contextual transitions on specific links with the data-transition attribute to override both pathname-based and default transitions:

<a href="/gallery" data-transition="slideUp">Gallery</a>

When this link is clicked, SlideUp is used regardless of what pathname-based or default transition would apply.

Priority (highest to lowest):

  1. data-transition attribute on the link (click navigation only)
  2. Pathname-based match in the transitions map
  3. _popstate direction transition (back/forward only)
  4. default transition

With _popstate.priority: true, items 2 and 3 swap — popstate transitions win over pathname matches. See Popstate Transitions below.

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.

Popstate Priority

By default, pathname-based transitions take precedence over popstate transitions. If you have both about: SlideUp and _popstate.back: SlideRight, pressing back to /about will use SlideUp.

Set priority: true to make popstate transitions win over pathname matches:

const core = new Core({
  transitions: {
    default: Fade,
    about: SlideUp,
    _popstate: {
      back: SlideRight,
      forward: SlideLeft,
      priority: true,  // popstate transitions win over pathname-assigned transitions
    }
  }
});

With priority: true, pressing back to /about uses SlideRight instead of SlideUp. Defaults to false.

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.

Precaching

pjax-max does not prefetch pages on its own. If your host environment already has HTML for a page (for example, from a CDN warm-up or a client-side prefetch strategy), hand it to pjax-max via core.precache(url, html) so navigation to that URL skips the network entirely.

const response = await fetch('/about');
const html = await response.text();
core.precache('/about', html);

Signature

core.precache(url: string, html: string): boolean
  • url — Absolute or relative URL. Resolved against window.location.href.
  • html — Full HTML string for the page at that URL.
  • Returns true if the entry was added to the cache, false otherwise.

Behavior

  • Same-origin only. Cross-origin URLs are rejected and return false.
  • Dedupes by URL. If the resolved URL is already in core.cache, precache returns false and does not overwrite the existing entry.
  • No fetch. The HTML is parsed and stored directly — no network request is made.
  • Invalid input safe. Non-string arguments or unparseable URLs return false instead of throwing.

When a navigation targets a precached URL, the out transition still runs as normal, but the new view is rendered from cache without waiting on the network.

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

License

MIT