Lightweight AJAX navigation library — accelerate your website without a JS framework.
µJS intercepts clicks on links and form submissions to load pages via AJAX instead of full browser navigation. The fetched content replaces part (or all) of the current page, making navigation faster and smoother.
No build step required. No dependencies. No framework. Just a single <script> tag.
Inspired by pjax, Turbo and HTMX, µJS aims to be simpler and lighter while covering the most common use cases.
- 🚀 Fast — Prefetch on hover, no full page reload, progress bar
- 🪶 Lightweight — Single file, ~10 KB gzipped, zero dependencies
- 🔌 Drop-in — Works with any backend (PHP, Python, Ruby, Go…), no server-side changes needed
- 🧩 Patch mode — Update multiple page fragments in a single request
- 🎯 Triggers — Any element, any event: live search, polling, focus actions
- 🔄 HTTP verbs — GET, POST, PUT, PATCH, DELETE on links, buttons, and forms
- 📡 SSE — Real-time updates via Server-Sent Events
- ✨ Modern — View Transitions, DOM morphing (via idiomorph),
fetchAPI, event delegation
- Installation
- Quick start
- Modes
- Patch mode
- Forms
- HTTP methods
- Triggers
- Server-Sent Events (SSE)
- Ghost mode
- Scroll restoration
- Prefetch
- DOM morphing
- View Transitions
- Progress bar
- Events
- Attributes reference
- Configuration reference
- Programmatic API
- Browser support
- License
<script src="/path/to/mu.min.js"></script>
<script>mu.init();</script><!-- unpkg -->
<script src="https://unpkg.com/@digicreon/mujs/dist/mu.min.js"></script>
<!-- jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/@digicreon/mujs/dist/mu.min.js"></script>npm install @digicreon/mujsAfter calling mu.init(), all internal links (URLs starting with /) are automatically intercepted. Clicking a link fetches the page via AJAX and replaces the current <body> with the fetched <body>. The page title is updated automatically. Browser history (back/forward buttons) works as expected.
<!DOCTYPE html>
<html>
<head>
<title>My site</title>
<script src="/path/to/mu.min.js"></script>
<script>mu.init();</script>
</head>
<body>
<!-- These links are automatically handled by µJS -->
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
<main id="content">
<p>Page content here.</p>
</main>
<!-- This link is NOT handled (external URL) -->
<a href="https://example.com">External link</a>
<!-- This link is NOT handled (explicitly disabled) -->
<a href="/file.pdf" mu-disabled>Download PDF</a>
</body>
</html>To replace only a fragment of the page instead of the whole body:
<a href="/about" mu-target="#content" mu-source="#content">About</a>This fetches /about, extracts the #content element from the response, and replaces the current #content with it.
The mu-mode attribute controls how fetched content is injected into the page. Default: replace.
| Mode | Description |
|---|---|
replace |
Replace the target node with the source node (default). |
update |
Replace the inner content of the target with the source's inner content. |
prepend |
Insert the source node at the beginning of the target. |
append |
Insert the source node at the end of the target. |
before |
Insert the source node before the target. |
after |
Insert the source node after the target. |
remove |
Remove the target node (source is ignored). |
none |
Do nothing to the DOM (events are still fired). |
patch |
Process multiple targeted fragments (see Patch mode). |
Example:
<a href="/notifications" mu-mode="update" mu-target="#notifs" mu-source="#notifs">
Refresh notifications
</a>Patch mode allows a single request to update multiple parts of the page. The server returns HTML fragments, each annotated with a target and an optional mode.
<a href="/api/comments/new" mu-mode="patch">Add comment</a>The server returns plain HTML. Each element with a mu-patch-target attribute is a patch fragment:
<!-- Replaces #comment-42 (default mode: replace) -->
<div class="comment" mu-patch-target="#comment-42">
Updated comment text
</div>
<!-- Appends a new comment to #comments -->
<div class="comment" mu-patch-target="#comments" mu-patch-mode="append">
New comment
</div>
<!-- Updates the page title -->
<title mu-patch-target="title">New page title</title>
<!-- Adds a stylesheet -->
<link rel="stylesheet" href="/css/gallery.css"
mu-patch-target="head" mu-patch-mode="append">
<!-- Removes an element -->
<div mu-patch-target="#old-banner" mu-patch-mode="remove"></div>Patch fragments are standard HTML elements — no special tags needed. The mu-patch-* attributes are preserved on injected nodes for debugging.
The mu-patch-mode attribute accepts the same values as mu-mode (except patch and none). Default is replace.
By default, patch mode does not modify browser history. To add the URL to history:
<a href="/products?cat=3" mu-mode="patch" mu-patch-ghost="false">Filter</a>µJS intercepts form submissions. HTML5 validation (reportValidity()) is checked before any request.
Data is serialized as a query string. Behaves like a link.
<form action="/search" method="get" mu-target="#results" mu-source="#results">
<input type="text" name="q">
<button type="submit">Search</button>
</form>Data is sent as FormData. Ghost mode is enabled by default (POST responses should not be replayed via the browser back button).
<form action="/comment/create" method="post">
<textarea name="body"></textarea>
<button type="submit">Send</button>
</form>Use mu-method to override the HTTP method. The form data is sent as FormData, like POST.
<!-- PUT form -->
<form action="/api/user/1" mu-method="put">
<input type="text" name="name">
<button type="submit">Update</button>
</form>
<!-- DELETE form (no data needed) -->
<form action="/api/user/1" mu-method="delete">
<button type="submit">Delete</button>
</form><form action="/comment/create" method="post" mu-mode="patch">
<textarea name="body"></textarea>
<button type="submit">Send</button>
</form>Server response:
<div class="comment" mu-patch-target="#comments" mu-patch-mode="append">
<p>The new comment</p>
</div>
<form action="/comment/create" method="post" mu-patch-target="#comment-form">
<textarea name="body"></textarea>
<button type="submit">Send</button>
</form>The new comment is appended to the list, and the form is replaced with a blank version.
<form action="/save" method="post" mu-validate="myValidator">...</form>
<script>
function myValidator(form) {
return form.querySelector('#name').value.length > 0;
}
</script>Add mu-confirm-quit to a form. If any input is modified, the user is prompted before navigating away:
<form action="/save" method="post" mu-confirm-quit>
<input type="text" name="title">
<button type="submit">Save</button>
</form>By default, links use GET and forms use their method attribute. The mu-method attribute overrides the HTTP method for any element.
Supported values: get, post, put, patch, delete, sse.
<!-- DELETE button -->
<button mu-url="/api/item/42" mu-method="delete" mu-mode="remove" mu-target="#item-42">
Delete
</button>
<!-- PUT link -->
<a href="/api/publish/5" mu-method="put" mu-mode="none">Publish</a>Non-GET requests send an X-Mu-Method header with the HTTP method, allowing the server to distinguish between standard and µJS-initiated requests.
Note:
mu-postis deprecated. Usemu-method="post"instead.
µJS supports custom event triggers via the mu-trigger attribute. This allows any element with a mu-url to initiate a fetch on events other than click or submit.
When mu-trigger is absent, the trigger depends on the element type:
| Element | Default trigger |
|---|---|
<a> |
click |
<form> |
submit |
<input>, <textarea>, <select> |
change |
| Any other element | click |
| Trigger | Browser event(s) | Typical elements |
|---|---|---|
click |
click |
Any element (default for <a>, <button>, <div>...) |
submit |
submit |
<form> |
change |
input |
<input>, <textarea>, <select> |
blur |
change + blur (deduplicated) |
<input>, <textarea>, <select> |
focus |
focus |
<input>, <textarea>, <select> |
load |
(fires immediately when rendered) | Any element |
Live search with debounce:
<input type="text" name="q"
mu-trigger="change" mu-debounce="500"
mu-url="/search" mu-target="#results" mu-source="#results"
mu-mode="update">Action on focus (e.g. load suggestions):
<input type="text" mu-trigger="focus"
mu-url="/suggestions" mu-target="#suggestions" mu-mode="update">Action on blur (save on field exit):
<input type="text" name="title" mu-trigger="blur"
mu-url="/api/save" mu-method="put" mu-target="#status" mu-mode="update">Load content immediately:
<div mu-trigger="load"
mu-url="/sidebar" mu-target="#sidebar" mu-mode="update">
</div>Combine mu-trigger="load" with mu-repeat to poll a URL at regular intervals:
<div mu-trigger="load" mu-repeat="5000"
mu-url="/notifications" mu-target="#notifs" mu-mode="update">
</div>The first fetch fires immediately, then every 5 seconds. Polling intervals are automatically cleaned up when the element is removed from the DOM.
Use mu-debounce to delay the fetch until the user stops interacting:
<input type="text" name="q" mu-debounce="300"
mu-url="/search" mu-target="#results" mu-mode="update">Note: Triggers other than
clickandsubmitdefault to ghost mode (no browser history entry).
µJS supports real-time updates via Server-Sent Events. Set mu-method="sse" to open an EventSource connection instead of a one-shot fetch.
<div mu-trigger="load" mu-url="/chat/stream"
mu-mode="patch" mu-method="sse">
</div>Each incoming SSE message is treated as HTML and rendered according to the element's mu-mode. In patch mode, the server sends HTML fragments with mu-patch-target attributes, just like a regular patch response.
event: message
data: <div mu-patch-target="#messages" mu-patch-mode="append"><p>New message!</p></div>
event: message
data: <span mu-patch-target="#online-count">42</span>
- No custom headers:
EventSourcedoes not support custom HTTP headers. Use query parameters for authentication (e.g.mu-url="/stream?token=abc"). - Connection limit: Browsers allow ~6 SSE connections per domain in HTTP/1.1. Use HTTP/2 to avoid this limit.
- Automatic cleanup: SSE connections are closed when the element is removed from the DOM (e.g. when the page changes).
Ghost mode prevents a navigation from being added to browser history and disables automatic scroll-to-top.
<!-- Ghost mode on a single link -->
<a href="/panel" mu-ghost>Open panel</a>
<!-- Ghost mode globally -->
<script>mu.init({ ghost: true });</script>In patch mode, ghost is enabled by default. Use mu-patch-ghost="false" to add the URL to history.
When the user navigates with the browser's back/forward buttons, µJS automatically restores the scroll position to where it was before leaving the page. This works out of the box — no configuration needed.
When enabled (default), µJS fetches the target page when the user hovers over a link, before they click. This saves ~100-300ms of perceived loading time.
The prefetch cache stores one entry per URL and is consumed on click.
<!-- Disable prefetch on a specific link -->
<a href="/heavy-page" mu-prefetch="false">Heavy page</a>
<!-- Disable prefetch globally -->
<script>mu.init({ prefetch: false });</script>When a morph library is available, µJS uses it for replace and update modes to preserve DOM state (input focus, scroll positions, video playback, CSS transitions, etc.).
µJS auto-detects idiomorph. Just load it before µJS:
<script src="/path/to/idiomorph.js"></script>
<script src="/path/to/mu.min.js"></script>
<script>mu.init();</script>If idiomorph is not loaded, µJS falls back to direct DOM replacement. No error, no warning.
Disable morphing globally or per-element:
<!-- Globally -->
<script>mu.init({ morph: false });</script>
<!-- Per-element -->
<a href="/page" mu-morph="false">Link</a>To use a different morph library:
mu.init();
mu.setMorph(function(target, html, opts) {
myMorphLib.morph(target, html, opts);
});µJS uses the View Transitions API when supported by the browser, providing smooth animated transitions between page states.
Enabled by default. Falls back silently on unsupported browsers.
<!-- Disable globally -->
<script>mu.init({ transition: false });</script>
<!-- Disable per-element -->
<a href="/page" mu-transition="false">Link</a>A thin progress bar (3px, blue) is displayed at the top of the page during fetch requests. It requires no external stylesheet.
The bar element has id="mu-progress" and can be customized via CSS:
#mu-progress {
background: red !important;
height: 5px !important;
}Disable globally:
<script>mu.init({ progress: false });</script>µJS dispatches CustomEvent events on document. All events carry a detail object with lastUrl and previousUrl.
| Event | Cancelable | Description |
|---|---|---|
mu:init |
No | Fired after initialization. |
mu:before-fetch |
Yes | Fired before fetching. preventDefault() aborts the load. |
mu:before-render |
Yes | Fired after fetch, before DOM injection. detail.html can be modified. |
mu:after-render |
No | Fired after DOM injection. |
mu:fetch-error |
No | Fired on fetch failure or HTTP error. |
document.addEventListener("mu:after-render", function(e) {
console.log("Loaded: " + e.detail.url);
myApp.initWidgets();
});document.addEventListener("mu:before-fetch", function(e) {
if (e.detail.url === "/restricted") {
e.preventDefault();
}
});document.addEventListener("mu:before-render", function(e) {
e.detail.html = e.detail.html.replace("foo", "bar");
});document.addEventListener("mu:fetch-error", function(e) {
if (e.detail.status === 404) {
alert("Page not found");
}
});All attributes support both mu-* and data-mu-* syntax.
| Attribute | Description |
|---|---|
mu-disabled |
Disable µJS on this element. |
mu-mode |
Injection mode (replace, update, prepend, append, before, after, remove, none, patch). |
mu-target |
CSS selector for the target node in the current page. |
mu-source |
CSS selector for the source node in the fetched page. |
mu-url |
Override the URL to fetch (instead of href / action). |
mu-prefix |
URL prefix for the fetch request. |
mu-title |
Selector for the title node. Supports selector/attribute syntax. Empty string to disable. |
mu-ghost |
Skip browser history and scroll-to-top. |
mu-ghost-redirect |
Skip history for HTTP redirections. |
mu-scroll-to-top |
Force (true) or prevent (false) scrolling to top. |
mu-morph |
Disable morphing on this element (false). |
mu-transition |
Disable view transitions on this element (false). |
mu-prefetch |
Disable prefetch on hover for this link (false). |
mu-method |
HTTP method: get, post, put, patch, delete, or sse. |
mu-trigger |
Event trigger: click, submit, change, blur, focus, load. |
mu-debounce |
Debounce delay in milliseconds (e.g. "500"). |
mu-repeat |
Polling interval in milliseconds (e.g. "5000"). |
mu-post |
(Deprecated) Use mu-method="post" instead. |
mu-confirm |
Show a confirmation dialog before loading. |
mu-confirm-quit |
(Forms) Prompt before leaving if the form has been modified. |
mu-validate |
(Forms) Name of a JS validation function. Must return true/false. |
mu-patch-target |
(Patch fragments) CSS selector of the target node. |
mu-patch-mode |
(Patch fragments) Injection mode for this fragment. |
mu-patch-ghost |
Set to false to add the URL to browser history in patch mode. |
Pass an object to mu.init() to override defaults:
mu.init({
ghost: true,
processForms: false,
morph: false,
progress: true
});| Option | Type | Default | Description |
|---|---|---|---|
processLinks |
bool | true |
Intercept <a> tags. |
processForms |
bool | true |
Intercept <form> tags. |
ghost |
bool | false |
Ghost mode for all navigations. |
ghostRedirect |
bool | false |
Ghost mode for HTTP redirections. |
mode |
string | "replace" |
Default injection mode. |
target |
string | "body" |
Default target CSS selector. |
source |
string | "body" |
Default source CSS selector. |
title |
string | "title" |
Title selector ("selector" or "selector/attribute"). |
scrollToTop |
bool|null | null |
Scroll behavior. null = auto (scroll unless ghost). |
urlPrefix |
string|null | null |
Prefix added to fetched URLs. |
progress |
bool | true |
Show progress bar during fetch. |
prefetch |
bool | true |
Prefetch pages on link hover. |
morph |
bool | true |
Enable DOM morphing (requires idiomorph or custom morph function). |
transition |
bool | true |
Enable View Transitions API. |
confirmQuitText |
string | "Are you sure you want to leave this page?" |
Quit-page confirmation message. |
// Load a page programmatically
mu.load("/page", { ghost: true, target: "#content" });
// Get the last URL loaded by µJS
mu.getLastUrl(); // "/about" or null
// Get the previous URL
mu.getPreviousUrl(); // "/" or null
// Enable/disable quit-page confirmation
mu.setConfirmQuit(true);
mu.setConfirmQuit(false);
// Register a custom morph function
mu.setMorph(function(target, html, opts) {
myMorphLib.morph(target, html, opts);
});µJS works in all modern browsers:
- Chrome / Edge 89+
- Firefox 87+
- Safari 15+
View Transitions require Chrome/Edge 111+. On unsupported browsers, transitions are skipped silently.
DOM morphing requires a separate library (idiomorph recommended). Without it, µJS falls back to direct DOM replacement.
µJS does not support Internet Explorer.