
Taxi.js is a JavaScript PJAX navigation library for adding animated page transitions to server-rendered websites. URL-based routing can choose different transitions for specific navigation directions, with page prefetching and caching available for repeat visits.
Taxi replaces the current data-taxi-view content and exposes Renderer hooks for page-specific JavaScript. Native View Transitions, selective asset reloading, and programmatic navigation are available when a project needs them.
Features
- PJAX navigation for same-origin links.
- URL-based transition routing.
- Custom Transition classes or native View Transitions.
- Renderer lifecycle hooks for page-specific JavaScript.
- Hover or viewport-based page prefetching.
- Page caching with an optional LRU size limit.
- Selective JavaScript and stylesheet reloading.
- Programmatic navigation and browser history controls.
- Optional page-title announcements and focus management.
Use Cases
- Portfolio and WebGL sites that need different animations for specific source and destination URLs.
- CMS sites with page-specific JavaScript or CSS that must load after a PJAX page swap.
- Search and archive pages that need explicit cache refresh, bypass, or eviction controls.
How To Use It
Installation
Install the package from npm:
npm i @unseenco/taxi
For direct browser use, load Taxi.js through an ES module CDN.
<script type="module">
import { Core } from 'https://cdn.jsdelivr.net/npm/@unseenco/taxi@2/+esm';
const taxi = new Core();
</script>
Basic Usage
Every page needs a data-taxi wrapper and one data-taxi-view inside it. The view must be the wrapper’s only child.
After Core is created, eligible same-origin links fetch the destination document and replace the current data-taxi-view.
<main data-taxi>
<article data-taxi-view>
<h1>Home</h1>
<p>Page content goes here.</p>
<a href="/about/">About</a>
</article>
</main>
<script type="module">
import { Core } from 'https://cdn.jsdelivr.net/npm/@unseenco/taxi@2/+esm';
const taxi = new Core();
</script>
Taxi leaves these links and clicks to normal browser behavior:
- Links with
data-taxi-ignore. - Current-page anchor links.
- Links with a
targetattribute. - Links with a
downloadattribute. - Modified clicks using Command, Control, Shift, or Alt.
- Clicks where another handler has already called
preventDefault().
<a href="/contact/">Taxi navigation</a> <a href="/downloads/file.zip" download> Normal download </a> <a href="/checkout/" data-taxi-ignore> Normal page load </a>
Use Native View Transitions
Set enableViewTransitions to true to run Taxi’s page swap through document.startViewTransition() when the browser implements the API.
Custom JavaScript Transition classes are bypassed during a native View Transition. Renderer lifecycle hooks continue to run. Browsers that lack the API use Taxi’s regular Transition behavior.
When prefers-reduced-motion: reduce is active, Taxi swaps the content with no View Transition animation. onEnterCompleted() and NAVIGATE_END wait for the native transition to finish when an animation does run.
import { Core } from '@unseenco/taxi';
const taxi = new Core({
enableViewTransitions: true
});
Use matching view-transition-name values when an individual object should transition between two pages.
.hero-image {
view-transition-name: hero-image;
}
Create A Custom JavaScript Transition
Extend Transition when the animation needs JavaScript control.
onLeave() receives the outgoing view as from. onEnter() receives the incoming view as to. A hook can call done() or return a Promise when its animation finishes.
import { Core, Transition } from '@unseenco/taxi';
class FadeTransition extends Transition {
onLeave({ from }) {
return from.animate(
[
{ opacity: 1 },
{ opacity: 0 }
],
{
duration: 250,
easing: 'ease'
}
).finished;
}
onEnter({ to }) {
return to.animate(
[
{ opacity: 0 },
{ opacity: 1 }
],
{
duration: 250,
easing: 'ease'
}
).finished;
}
}
const taxi = new Core({
transitions: {
default: FadeTransition
}
});
Choose A Transition
Taxi checks transition sources in this order:
data-transitionon the clicked link.- A matching URL route.
- The default registered Transition.
Register Transition classes under unique keys:
const taxi = new Core({
transitions: {
default: FadeTransition,
gallery: GalleryTransition,
article: ArticleTransition
}
});
A link can request a registered Transition directly:
<a href="/gallery/" data-transition="gallery"> Open gallery </a>
Route Transitions By URL
addRoute() selects a Transition from the current pathname and destination pathname.
Taxi anchors each route expression to the entire pathname and removes trailing slashes before matching. The homepage therefore uses an empty string. Rules run in declaration order, so put specific routes before catch-all expressions.
taxi.addRoute('/blog/.*', '', 'blogToHome');
taxi.addRoute('', '/blog/.*', 'homeToBlog');
taxi.addRoute('/projects/.*', '/projects/.*', 'projectToProject');
A specific rule should appear before a wider expression:
taxi.addRoute('/projects/featured', '', 'featuredToHome');
taxi.addRoute('/projects/.*', '', 'projectToHome');
Run Page-Specific JavaScript With Renderers
A Renderer manages JavaScript associated with a page type. Put the registered Renderer key in data-taxi-view.
The base initialLoad() method does not call onEnter() or onEnterCompleted() in the current release. Call those hooks from a custom initialLoad() when the first page visit needs the regular enter setup.
import { Core, Renderer } from '@unseenco/taxi';
class ArticleRenderer extends Renderer {
initialLoad() {
this.onEnter();
this.onEnterCompleted();
}
onEnter() {
initArticleWidgets();
}
onLeave() {
destroyArticleWidgets();
}
}
const taxi = new Core({
renderers: {
default: Renderer,
article: ArticleRenderer
}
});
<main data-taxi>
<article data-taxi-view="article">
...
</article>
</main>
Renderer Lifecycle Hooks
| Hook | Runs When |
|---|---|
initialLoad() | The site first loads with this Renderer active. |
onLeave() | The current page begins leaving. |
onLeaveCompleted() | The leave Transition has completed. |
onEnter() | The incoming content has entered the Taxi wrapper. |
onEnterCompleted() | The enter Transition has completed. |
Renderer Properties
| Property | Description |
|---|---|
this.page | Full document associated with the Renderer. |
this.title | Document title for the rendered page. |
this.wrapper | Main data-taxi wrapper. |
this.content | Active data-taxi-view. |
this.trigger | Navigation trigger, including a link element, 'popstate', 'initialLoad', or false. |
Transition Hooks
this.wrapper inside a Transition references the main data-taxi wrapper.
| Hook | Parameters |
|---|---|
onLeave() | { from, trigger, done } |
onEnter() | { to, trigger, done } |
Prefetch Pages
Taxi prefetches links on mouseenter and focus by default.
Set enablePrefetch to 'visible' to preload matching same-origin links when they enter the viewport. Visible prefetching does not run when navigator.connection.saveData reports Data Saver mode.
const taxi = new Core({
enablePrefetch: 'visible'
});
Disable automatic prefetching with:
const taxi = new Core({
enablePrefetch: false
});
preload(url, preloadAssets) can prime a URL manually. Pass true as the second argument when Taxi should create the incoming DOM early so referenced assets can begin loading.
taxi.preload('/gallery/');
taxi.preload('/gallery/', true);
preload() resolves with the resulting CacheEntry and rejects after a failed request or a fetched page with no data-taxi-view.
taxi.preload('/gallery/')
.then((entry) => {
console.log(entry.title);
})
.catch((error) => {
console.error(error);
});
Control The Page Cache
Fetched pages enter Taxi’s cache by default.
maxCacheSize sets a positive cache limit and evicts the least recently used non-current entry. The default 0 leaves the cache size unlimited.
const taxi = new Core({
maxCacheSize: 10
});
Set bypassCache to disable the page cache for the entire Core instance:
const taxi = new Core({
bypassCache: true
});
Place data-taxi-nocache on a view that should always be fetched again:
<article data-taxi-view data-taxi-nocache> ... </article>
Use updateCache() after dynamic DOM changes that need to become the current cached state. Both cache methods accept an optional URL.
taxi.updateCache();
taxi.updateCache('/search/');
taxi.clearCache();
taxi.clearCache('/search/');
Reload Page-Specific JavaScript And CSS
Taxi processes scripts, stylesheets, and inline styles marked with data-taxi-reload by default.
<script src="/js/gallery.js" data-taxi-reload></script> <link rel="stylesheet" href="/css/gallery.css" data-taxi-reload >
Set reloadJsFilter or reloadCssFilter to a custom predicate when the attribute rule is not suitable. Set either option to false to disable that asset type.
const taxi = new Core({
reloadJsFilter: (element) => {
return element.dataset.taxiReload !== undefined ||
element.src?.includes('gallery.js');
},
reloadCssFilter: (element) => {
return element.dataset.taxiReload !== undefined;
}
});
Enable Navigation Accessibility Handling
Set enableAccessibility to true when PJAX navigation should announce the destination title and restore useful keyboard focus after each page swap.
Taxi creates an assertive aria-live region for the page title. Focus moves to the first <h1> inside the incoming view, or to data-taxi-view when no <h1> exists. Taxi sets tabindex="-1" when the focus destination does not already have a tabindex.
const taxi = new Core({
enableAccessibility: true
});
Listen For Navigation Events
All three events receive a payload containing from, to, and trigger.
For NAVIGATE_OUT, the destination to value can be a stub when Taxi has not cached the destination yet. In that state, finalUrl is available and the other destination fields can be null.
| Event | Fires When |
|---|---|
NAVIGATE_OUT | Navigation begins before the outgoing Transition runs. |
NAVIGATE_IN | The incoming view has entered the document. |
NAVIGATE_END | The incoming Transition has completed. |
taxi.on('NAVIGATE_OUT', ({ from, to, trigger }) => {
console.log('Leaving:', from.finalUrl);
});
taxi.on('NAVIGATE_IN', ({ to }) => {
console.log('New view:', to.finalUrl);
});
taxi.on('NAVIGATE_END', ({ to }) => {
console.log('Finished:', to.finalUrl);
});
Pass the original callback to off() to remove one listener. Omit the callback to remove every listener registered for that event.
function handleNavigation(data) {
console.log(data.to.finalUrl);
}
taxi.on('NAVIGATE_END', handleNavigation);
taxi.off('NAVIGATE_END', handleNavigation);
taxi.off('NAVIGATE_IN');
Programmatic Navigation
navigateTo() accepts the destination URL, an optional registered Transition key, and an optional trigger value.
taxi.navigateTo('/contact/');
taxi.navigateTo('/contact/', 'article');
taxi.navigateTo('/contact/', 'article', 'menu');
Taxi exposes guarded browser history methods too:
taxi.navigateBack(); taxi.navigateForward();
Configuration Options
renderers(Record<string, Renderer>, default{ default: Renderer }): Registers Renderer classes by key.transitions(Record<string, Transition>, default{ default: Transition }): Registers Transition classes by key.links(string, default"a[href]:not([target]):not([href^=\\#]):not([data-taxi-ignore])"): CSS selector used for delegated navigation links.removeOldContent(boolean, defaulttrue): Removes the outgoing view after its leave Transition.allowInterruption(boolean, defaultfalse): Lets a new navigation interrupt the active navigation.bypassCache(boolean, defaultfalse): Disables Taxi’s page cache.enablePrefetch(false | 'hover' | 'visible', default'hover'): Selects the automatic prefetch mode. The legacy boolean valuetruemaps to'hover'.enableViewTransitions(boolean, defaultfalse): Usesdocument.startViewTransition()when the browser has the API.enableAccessibility(boolean, defaultfalse): Announces the new page title and moves focus after navigation.maxCacheSize(number, default0): Limits cached pages with least-recently-used eviction.0leaves the cache unlimited.fetchOptions(RequestInit, default{}): Applies Fetch API options to Taxi requests.reloadJsFilter(boolean | function): Selects incoming scripts for execution. The default predicate matchesdata-taxi-reload.reloadCssFilter(boolean | function): Selects incoming<link rel="stylesheet">and<style>nodes for processing. The default predicate matchesdata-taxi-reload.
Core API Methods
| Method | Description |
|---|---|
addRoute(fromPattern, toPattern, transition) | Registers a URL-based Transition rule. |
navigateTo(url, transition?, trigger?) | Runs programmatic navigation and returns Promise<void>. |
navigateBack() | Calls browser back navigation after checking the active-transition guard. |
navigateForward() | Calls browser forward navigation after checking the active-transition guard. |
preload(url, preloadAssets?) | Fetches a page into the cache and resolves with its CacheEntry. |
updateCache(url?) | Rebuilds cached HTML for a URL or the current page. |
clearCache(url?) | Removes a URL or the current page from the cache. |
destroy() | Removes Taxi’s navigation listeners and observers, aborts its active request, removes the accessibility announcer, and clears its cache. |
setDefaultRenderer(renderer) | Changes the default registered Renderer. |
setDefaultTransition(transition) | Changes the default registered Transition. |
on(event, callback) | Registers a Taxi navigation event listener. |
off(event, callback?) | Removes one callback or all callbacks for an event. |
Callbacks registered through taxi.on() are not removed by destroy(). Remove those listeners with taxi.off() when the surrounding application lifecycle requires cleanup.
`currentCacheEntry`
currentCacheEntry is a read-only getter for the active page’s CacheEntry.
A CacheEntry uses these fields:
| Field | Description |
|---|---|
renderer | Renderer instance associated with the page. |
page | Source Document or Node. |
scripts | Reloadable script elements collected from the page. |
styles | Reloadable stylesheet and style elements collected from the page. |
finalUrl | Final resolved page URL. |
skipCache | Indicates that the page has data-taxi-nocache. |
title | Document title. |
content | Page’s data-taxi-view element. |
Public Data Attributes
| Attribute | Purpose |
|---|---|
data-taxi | Marks the persistent wrapper around replaceable page content. |
data-taxi-view | Marks replaceable page content. Its value can select a registered Renderer. |
data-taxi-ignore | Excludes a link from Taxi navigation. |
data-transition | Selects a registered Transition for a link click. |
data-taxi-nocache | Prevents a page view from being reused from Taxi’s cache. |
data-taxi-reload | Marks a script, stylesheet, or inline style for processing after navigation. |
Alternatives & Related Resources
- Create CSS Transitions When Switching Between Pages – swup
- Smooth Page Transitions With JavaScript And PJAX – barba.js
- Lightweight AJAX Page Navigation Library – µJS
FAQs
Q: Why does JavaScript or CSS from the next page not run after navigation?
A: Taxi processes incoming scripts and styles marked with data-taxi-reload by default. Change reloadJsFilter or reloadCssFilter when the project needs different asset rules.
Q: Can Taxi.js be imported by code that also runs during server-side rendering?
A: Yes. Importing the package no longer requires a DOM. Create the Core instance in the browser because initialization needs document, navigation APIs, and a data-taxi wrapper.
Q: Why does a link perform a normal page load?
A: Check for data-taxi-ignore, target, download, current-page hash URLs, modified clicks, cross-origin destinations, or an earlier handler that called preventDefault().
Q: Does Taxi.js manage keyboard focus after a PJAX page change?
A: Set enableAccessibility: true. Taxi announces the destination title and focuses the incoming page’s first <h1>, or the view itself when no <h1> exists.







