pjax-navigate
v0.0.4
Published
Framework-agnostic client-side navigation for multi-page apps using the View Transitions API, inspired by Astro's ClientRouter
Maintainers
Readme
pjax-navigate
Framework-agnostic client-side navigation for multi-page apps (MPA), powered by the View Transitions API.
Intercepts same-origin link clicks and form submissions, fetches the next page, and swaps the document in place — giving a server-rendered site SPA-like navigation without a client-side framework. Browsers without View Transitions support get an animated or instant fallback.
[!NOTE] Inspired by and adapted from Astro's
<ClientRouter />(view transitions router), decoupled from Astro so it can be used with any server-rendered HTML.
Features
- Soft navigation for links (
<a>,<area>, SVG<a>) and forms (GETandPOST) - Native view transitions, with an
animate/swap/nonefallback for other browsers - Back / forward support with scroll restoration
- Persistent elements across navigations (state, focus, media playback and
<canvas>contexts are kept) - Re-executes new scripts after a swap, with opt-in re-running of already executed ones
- Link prefetching with
tap,hover,viewportandloadstrategies - Lifecycle events to customize loading and swapping
- Announces the new page title to screen readers
Installation
npm install pjax-navigateUsage
Opt a page in by adding the pjax:enabled meta tag. Navigation only happens between pages that both have it; anything else falls back to a regular full page load.
<head>
<meta name="pjax:enabled" />
</head>Initialize the navigator once on the client:
import { initializeNavigator } from "pjax-navigate";
initializeNavigator();Navigate programmatically:
import { navigate } from "pjax-navigate";
await navigate("/about");
await navigate("/search?q=pjax", { history: "replace" });Options
initializeNavigator({
// Treat the environment as server-side (no-op). Defaults to `typeof window === "undefined"`.
ssr: false,
// Enables dev-only behaviour: debug logs, Vite / Vue / Svelte dev style handling.
dev: false,
// Enables link prefetching (see "Prefetching").
prefetch: false,
// In dev mode, collect client-only styles of the next page before swapping.
styles: true,
// Custom fetch used for navigations and prefetching.
// `trigger` is `"prefetch"` for prefetch requests, otherwise `null`.
fetch: (url, { trigger, ...init }) => fetch(url, init),
});navigate(href, options?)
| Option | Type | Description |
| --------------- | ----------------------------------- | ----------------------------------------------------------- |
| history | "auto" | "push" | "replace" | How to update the history. Defaults to pushing a new entry. |
| state | any | Extra state stored in the history entry. |
| info | any | Arbitrary data passed to the navigation events. |
| formData | FormData | Sends a POST request with this body. |
| sourceElement | Element | The element that triggered the navigation. |
Attributes
| Attribute | On | Description |
| --------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------- |
| data-pjax-persist="<id>" | any element | Keeps the element (and its state) across navigations when the next page has a matching id. |
| data-pjax-persist | <head> elements | Always keeps the element in <head>, even if the next page doesn't contain it. Ignored on body elements. |
| data-pjax-reload | links, <form> | Skips soft navigation and lets the browser do a full page load. |
| data-pjax-history="replace" | links | Replaces the current history entry instead of pushing a new one. |
| data-pjax-rerun | <script> | Re-executes the script on every navigation, even if it already ran. |
| data-pjax-prefetch="<strategy>" | <a> | Prefetches the link using tap, hover, viewport or load. An empty value uses hover; "false" opts out. |
The navigator also sets these on <html> during a navigation, which you can use in your CSS:
| Attribute | Values | Description |
| ---------------------- | ------------------- | ---------------------------------------------------------- |
| data-pjax-transition | forward | back | Direction of the current navigation. |
| data-pjax-fallback | old | new | Phase of the fallback animation (non-supporting browsers). |
Fallback
In browsers without the View Transitions API, the behaviour is controlled per page with the pjax:fallback meta tag:
<meta name="pjax:fallback" content="animate" />animate(default) — runs animations triggered by[data-pjax-fallback="old"]and[data-pjax-fallback="new"]on<html>, waiting for them to finish before and after the swap.swap— swaps the page without animations.none— disables soft navigation; the browser does full page loads.
html[data-pjax-fallback="old"] main {
animation: fade-out 150ms ease-out both;
}
html[data-pjax-fallback="new"] main {
animation: fade-in 150ms ease-in both;
}Events
All events are dispatched on document and are fully typed through DocumentEventMap.
| Event | Event type | When |
| ------------------------- | ---------------------------------- | ----------------------------------------------------------- |
| pjax:before-preparation | TransitionBeforePreparationEvent | Before the next page is fetched. Cancelable. |
| pjax:after-preparation | Event | After the next page is fetched and parsed. |
| pjax:before-swap | TransitionBeforeSwapEvent | Before the document is swapped, inside the view transition. |
| pjax:after-swap | Event | Right after the swap, before new scripts run. |
| pjax:page-load | Event | After scripts ran, and on the initial page load. |
Override the loader, e.g. to show a progress bar or change the destination:
document.addEventListener("pjax:before-preparation", (event) => {
const originalLoader = event.loader;
event.loader = async () => {
showSpinner();
await originalLoader();
hideSpinner();
};
});Customize the swap, e.g. to keep the current theme:
document.addEventListener("pjax:before-swap", (event) => {
event.newDocument.documentElement.dataset.theme =
document.documentElement.dataset.theme;
});Run setup code on every page:
document.addEventListener("pjax:page-load", () => {
setupMenu();
});Calling event.preventDefault() on pjax:before-preparation falls back to a full page load.
Prefetching
With prefetch: true, all <a> links are prefetched on hover. Use data-pjax-prefetch to choose another strategy or data-pjax-prefetch="false" to opt out:
<!-- hover -->
<a href="/blog" data-pjax-prefetch>Blog</a>
<!-- viewport -->
<a href="/docs" data-pjax-prefetch="viewport">Docs</a>
<!-- load -->
<a href="/pricing" data-pjax-prefetch="load">Pricing</a>Prefetching is skipped when offline, for cross-origin URLs and for the current page. On data saver or 2G connections, links are only prefetched on tap.
Credits
This package is based on the view transitions router from Astro — licensed under MIT, Copyright (c) 2021–present Astro. See LICENSE.md.
