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-general

v1.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-general
import { 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):

  1. data-transition attribute on the link (click navigation only)
  2. _popstate direction transition (back/forward only)
  3. default transition

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 first mouseenter. Only fires once per link.

Programmatic Prefetch

core.prefetch([linkElement, '/contact']);  // accepts elements or URL strings

Behavior

  • 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.cache that 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

License

MIT