Documentation
Installation
Script tag (CDN)
The simplest way. Add these two lines to your HTML:
<script src="https://unpkg.com/@digicreon/mujs@1.4.3/dist/mu.min.js"></script>
<script>mu.init();</script>
You can also use jsDelivr:
<script src="https://cdn.jsdelivr.net/npm/@digicreon/mujs@1.4.3/dist/mu.min.js"></script>
<script>mu.init();</script>
npm
npm install @digicreon/mujs
Then import it in your JavaScript:
import mu from "@digicreon/mujs";
mu.init();
Quick start
After including µJS and calling mu.init(), all internal links 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="https://unpkg.com/@digicreon/mujs@1.4.3/dist/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>
By default, µJS replaces the entire <body>. To replace only a fragment of the page:
<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.
Link interception
µJS automatically intercepts clicks on <a> links and navigates via AJAX instead of a full page reload. However, not all links are intercepted — µJS applies a set of rules to determine which links it should handle.
Exclusion rules
A link is not intercepted if any of the following conditions is true:
- The
hrefdoes not start with/(e.g.https://...,mailto:,#anchor, relative paths) - The
hrefstarts with//(protocol-relative URL pointing to an external domain) - The link has a
targetattribute (e.g.target="_blank") - The link has a
downloadattribute - The link has a
mu-disabledattribute
Examples
<!-- Intercepted by µJS (internal, absolute path) -->
<a href="/about">About</a>
<a href="/contact">Contact</a>
<!-- NOT intercepted (external URL) -->
<a href="https://github.com/nicot/mujs">GitHub</a>
<!-- NOT intercepted (protocol-relative URL) -->
<a href="//cdn.example.com/file.js">CDN link</a>
<!-- NOT intercepted (mailto) -->
<a href="mailto:hello@example.com">Email us</a>
<!-- NOT intercepted (anchor) -->
<a href="#section">Jump to section</a>
<!-- NOT intercepted (target attribute) -->
<a href="/page" target="_blank">New tab</a>
<!-- NOT intercepted (download attribute) -->
<a href="/files/report.pdf" download>Download PDF</a>
<!-- NOT intercepted (explicitly disabled) -->
<a href="/legacy-page" mu-disabled>Legacy page</a>
The mu-disabled attribute is the recommended way to opt out a specific internal link from µJS interception. See Attributes reference for more details.
Server-side detection
µJS adds HTTP headers to every request, allowing your server to distinguish AJAX navigations from regular page loads. The most common use case is returning a partial HTML fragment instead of the full page, saving bandwidth and server-side rendering time.
Request headers
| Header | Value | Sent on |
|---|---|---|
X-Requested-With | XMLHttpRequest | All requests |
X-Mu-Mode | replace, append, patch, etc. | All requests |
X-Mu-Method | PUT, DELETE, etc. | Non-GET requests only |
X-Mu-Prefetch | 1 | Prefetch requests only |
The X-Requested-With header uses the standard XMLHttpRequest value, which is the de facto convention for identifying AJAX requests. Most server-side frameworks already provide a built-in helper to check for it (e.g. request.xhr? in Rails, request.is_xhr in Flask).
Example: returning a partial
Instead of rendering the full page layout (header, nav, footer), you can return just the content fragment when the request comes from µJS:
// PHP
if ($_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
// µJS request — return only the fragment
echo $fragment;
} else {
// Normal request — return the full page
include 'layout.php';
}
This is entirely optional — µJS works perfectly with full-page responses (it extracts the relevant fragment using mu-source). But returning partials can reduce response size and server load.
If you're coming from htmx, X-Requested-With: XMLHttpRequest serves the same purpose as the HX-Request: true header.
Target & Source
By default, µJS replaces the entire body. You can configure which part of the page is replaced (target) and which part of the response is extracted (source).
Global configuration
mu.init({
target: "main", // CSS selector — element to replace on the page
source: "main" // CSS selector — element to extract from the response
});
Per-link override
<a href="/page.html" mu-target="#content" mu-source="#content">Load</a>
The mu-target attribute sets the element to replace in the current page. The mu-source attribute sets the element to extract from the fetched page. Both accept CSS selectors.
If mu-source is not set, it falls back to the global source configuration, then to using the same selector as target.
Modes
The mu-mode attribute controls how fetched content is injected into the page. Default: replace.
| Mode | Behavior |
|---|---|
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
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.
Link triggering a patch
<a href="/api/comments/new" mu-mode="patch">Add comment</a>
Server response
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.
Patch and browser history
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-history="true">Filter</a>
Forms
µJS intercepts form submissions. HTML5 validation (reportValidity()) is checked before any request.
GET forms
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>
POST forms
Data is sent as FormData. History is disabled 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>
PUT / PATCH / DELETE forms
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>
POST form with patch response
<div id="comments">
<p>First comment</p>
</div>
<form id="comment-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 id="comment-form" action="/comment/create" method="post"
mu-patch-target="#comment-form"
mu-mode="patch">
<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.
Custom validation
<form action="/save" method="post" mu-validate="myValidator">...</form>
<script>
function myValidator(form) {
return form.querySelector('#name').value.length > 0;
}
</script>
Quit-page confirmation
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>
HTTP methods
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. See Server-side detection for the full list of headers sent by µJS.
Triggers
µ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.
Default triggers
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 |
Available triggers
| 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 |
Examples
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>
Polling
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.
Debounce
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 click and submit default to no browser history entry and no scroll (mu-history="false", mu-scroll="false").
Server-Sent Events (SSE)
µ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.
Server-side example
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>
Limitations
- 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).
History & Scroll
mu-history controls whether the URL is added to browser history. mu-scroll controls whether the page scrolls to top after rendering. Both attributes are independent.
<!-- Skip history on a link -->
<a href="/panel" mu-history="false">Open panel</a>
<!-- Skip history globally -->
<script>mu.init({ history: false });</script>
<!-- Scroll to top without adding history -->
<a href="/page" mu-history="false" mu-scroll="true">Link</a>
Defaults
Defaults for mu-history and mu-scroll depend on the mode and context:
| Mode | Context | mu-history |
mu-scroll |
|---|---|---|---|
replace, update | Links (GET) | true | true |
replace, update | Forms (GET) | true | true |
replace, update | Forms (POST/PUT/PATCH/DELETE) | false | true |
replace, update | Triggers (change, blur, focus, load) | false | false |
replace, update | SSE | false | false |
append, prepend, before, after, remove, none | Any | false | false |
patch | Any | false | false |
Redirections always add the URL to browser history, regardless of the mu-history setting. In patch mode, use mu-patch-history="true" to add the URL to history.
Scroll restoration
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.
Prefetch
µJS prefetches pages when the user hovers over a link. This saves ~100–300ms on click, making navigation feel nearly instant.
Default behavior
Prefetch is enabled by default. When the user hovers over a link, µJS waits 50ms before fetching the target page in the background — this avoids unnecessary requests when the mouse passes briefly over a link. The result is cached (one entry per URL, 3-second lifetime), and the cache is consumed on click.
Disable globally
mu.init({ prefetch: false });
Disable per-link
<a href="/page.html" mu-prefetch="false">No prefetch</a>
DOM morphing
DOM morphing updates the existing DOM to match the new content instead of replacing it entirely. This preserves focus, input values, scroll positions, CSS transitions, and video playback state.
How it works
µJS auto-detects the idiomorph library. When idiomorph is available on the page, µJS uses it for morphing automatically.
Enable/disable globally
mu.init({ morph: false });
Enable/disable per-link
<a href="/page.html" mu-morph="false">No morphing</a>
Custom morph function
mu.setMorph(function(target, html, opts) {
myMorphLib.morph(target, html, opts);
});
View Transitions
µJS supports the browser View Transitions API. When the browser supports it, page transitions are animated smoothly.
Default
View Transitions are enabled by default when the browser supports them.
Disable globally
mu.init({ transition: false });
Disable per-link
<a href="/page.html" mu-transition="false">No transition</a>
Progress bar
µJS displays a thin progress bar at the top of the page during AJAX requests. It provides visual feedback that content is loading.
Default
The progress bar is enabled by default.
Disable
mu.init({ progress: false });
Custom styling
The progress bar has the id #mu-progress. Override its styles in CSS:
#mu-progress {
background: #ff6600;
height: 3px;
}
Scripts
When µJS loads a page via AJAX, it automatically processes <script> tags found in the fetched content.
Execution rules
| Script type | Behavior |
|---|---|
Inline (<script>...</script>) | Re-executed on every navigation |
External (<script src="...">) | Executed only once — skipped on subsequent navigations if the same src was already loaded |
µJS tracks external scripts by their src URL. On the initial page load, all existing <script src="..."> are registered. When a fetched page includes an external script with the same src, it is not loaded again. This prevents libraries like jQuery or analytics scripts from being re-initialized on every page change.
New <link>, <style>, and <script> elements found in the fetched <head> are also merged into the current page's <head>, following the same deduplication rules.
Preventing script execution
Add mu-disabled to a <script> tag to prevent µJS from executing it during AJAX navigation:
<script mu-disabled>
// This script runs on initial page load (full page),
// but NOT when loaded via µJS navigation.
thirdPartyWidget.init();
</script>
This is useful for scripts that should only run once on a full page load (analytics snippets, third-party widgets) and must not be re-executed when the page content is fetched via µJS.
Events
µ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 |
Run code after each page load
document.addEventListener("mu:after-render", function(e) {
console.log("Loaded: " + e.detail.url);
myApp.initWidgets();
});
Cancel a navigation
document.addEventListener("mu:before-fetch", function(e) {
if (e.detail.url === "/restricted") {
e.preventDefault();
}
});
Modify HTML before rendering
document.addEventListener("mu:before-render", function(e) {
e.detail.html = e.detail.html.replace("foo", "bar");
});
Handle errors
document.addEventListener("mu:fetch-error", function(e) {
if (e.detail.status === 404) {
alert("Page not found");
}
});
Attributes reference
All attributes support both mu-* and data-mu-* syntax.
| Attribute | Description |
|---|---|
mu-disabled | Disable µJS on this element. On links and forms, prevents interception. On <script>, prevents execution during AJAX navigation (see Scripts). |
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-history | Add URL to browser history (true/false). Default depends on mode and context. |
mu-scroll | Force (true) or prevent (false) scrolling to top. Default depends on mode and context. |
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-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-history | Set to true to add the URL to browser history in patch mode. Default: false. |
Configuration reference
Options passed to mu.init():
| Option | Type | Default | Description |
|---|---|---|---|
processLinks | boolean | true | Intercept link clicks |
processForms | boolean | true | Intercept form submissions |
target | string | "body" | CSS selector for the default replacement target |
source | string | "body" | CSS selector for the default content source |
title | string | "title" | CSS selector for the page title element |
mode | string | "replace" | Default injection mode |
history | boolean | true | Add URL to browser history |
scroll | boolean|null | null | Scroll behavior. null = auto (depends on mode and context). |
urlPrefix | string | "" | Prefix added to all fetch URLs |
prefetch | boolean | true | Enable hover prefetch |
morph | boolean | true | Enable DOM morphing (when idiomorph is available) |
transition | boolean | true | Enable View Transitions API |
progress | boolean | true | Show progress bar during loading |
confirmQuitText | string | "Are you | Quit-page confirmation message |
Programmatic API
mu.init(config)
Initialize µJS with optional configuration. Must be called before any navigation occurs. See Configuration reference above for the list of available options.
mu.init({
target: "main",
source: "main",
prefetch: true
});
mu.load(url, config)
Programmatically navigate to a URL. Accepts the same options as mu.init() for per-request overrides.
mu.load("/dashboard.html");
// With overrides
mu.load("/fragment.html", {
target: "#sidebar",
source: "#sidebar-content",
history: false
});
mu.getLastUrl()
Returns the URL of the most recent µJS navigation.
var lastUrl = mu.getLastUrl();
mu.getPreviousUrl()
Returns the URL of the navigation before the current one.
var prevUrl = mu.getPreviousUrl();
mu.setConfirmQuit(enabled)
Programmatically enable or disable the confirm-quit prompt.
// Enable after user makes changes
mu.setConfirmQuit(true);
// Disable after saving
mu.setConfirmQuit(false);
mu.setMorph(fn)
Register a custom morph function. The function receives the target element, the new HTML, and an options object.
mu.setMorph(function(target, html, opts) {
myMorphLib.morph(target, html, opts);
});
Browser support
µJS works in all modern browsers. The minimum versions are determined by AbortController (used for request cancellation).
Desktop
| Browser | Version | Release date |
|---|---|---|
| Chrome | 66+ | April 2018 |
| Edge | 79+ | January 2020 |
| Firefox | 57+ | November 2017 |
| Safari | 12.1+ | March 2019 |
| Opera | 53+ | May 2018 |
Mobile
| Browser | Version | Release date |
|---|---|---|
| Chrome Android | 66+ | April 2018 |
| Safari iOS | 11.3+ | March 2018 |
| Firefox Android | 57+ | November 2017 |
| Opera Mobile | 47+ | May 2018 |
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 or legacy Edge (EdgeHTML).