Documentation

Installation #

Script tag (CDN) #

The simplest way. Add these two lines to your HTML:

<script src="https://unpkg.com/@digicreon/mujs@1.4.8/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.8/dist/mu.min.js"></script>
<script>mu.init();</script>

With Subresource Integrity (SRI):

<script src="https://unpkg.com/@digicreon/mujs@1.4.8/dist/mu.min.js"
        integrity="sha384-x9kao8F430kRoLUGlLYGRC2FPo+rCsVlMUItQA1zbf0ZsIcht6wv8nikdx/Prqh2"
        crossorigin="anonymous"></script>
<script>mu.init();</script>
<script src="https://cdn.jsdelivr.net/npm/@digicreon/mujs@1.4.8/dist/mu.min.js"
        integrity="sha384-x9kao8F430kRoLUGlLYGRC2FPo+rCsVlMUItQA1zbf0ZsIcht6wv8nikdx/Prqh2"
        crossorigin="anonymous"></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/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.

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-WithXMLHttpRequestAll requests
X-Mu-Modereplace, append, patch, etc.All requests
X-Mu-MethodPUT, DELETE, etc.Non-GET requests only
X-Mu-Prefetch1Prefetch 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
});
<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.

Relative targets (&) #

The & character in a selector is replaced by the ID selector of the triggering element. If the element has no id, one is auto-generated (prefixed mu-). This is useful for reusable components — lists of items, cards, table rows — where generating unique IDs server-side would be cumbersome.

<!-- Button that updates itself -->
<button mu-url="/api/status" mu-target="&">Refresh</button>

<!-- Element that updates a child -->
<div mu-trigger="click" mu-mode="update"
     mu-url="/api/list" mu-target="& > .list">
    <div class="list">...</div>
</div>

<!-- Button that updates an adjacent element -->
<button mu-url="/api/count" mu-target="& + .result">Load</button>
<div class="result"></div>

The & syntax also works in mu-patch-target for patch responses:

<!-- Server response: target relative to the trigger -->
<div mu-patch-target="& > .list" mu-patch-mode="update">...</div>

Inspired by CSS nesting syntax, the & is valid HTML5 in attribute values (no need for &amp; as it is not followed by a named entity).

Modes #

The mu-mode attribute controls how fetched content is injected into the page. Default: replace.

Mode Behavior
replaceReplace the target node with the source node (default)
updateReplace the inner content of the target with the source's inner content
prependInsert the source node at the beginning of the target
appendInsert the source node at the end of the target
beforeInsert the source node before the target
afterInsert the source node after the target
removeRemove the target node (source is ignored)
noneDo nothing to the DOM (events are still fired)
patchProcess 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. When a form has multiple submit buttons with different name/value attributes, µJS includes the clicked button's data in the submission — matching standard browser behavior.

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.

Non-GET clicks (via mu-method) do not use the prefetch cache and default to mu-history="false". This can be overridden with mu-history="true" if needed.

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:

ElementDefault trigger
<a>click
<form>submit
<input>, <textarea>, <select>change
Any other elementclick

Available triggers #

TriggerBrowser event(s)Typical elements
clickclickAny element (default for <a>, <button>, <div>...)
submitsubmit<form>
changeinput<input>, <textarea>, <select>
blurchange + blur (deduplicated)<input>, <textarea>, <select>
focusfocus<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: EventSource does 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, updateLinks (GET)truetrue
replace, updateLinks (POST/PUT/PATCH/DELETE via mu-method)falsetrue
replace, updateForms (GET)truetrue
replace, updateForms (POST/PUT/PATCH/DELETE)falsetrue
replace, updateTriggers (change, blur, focus, load)falsefalse
replace, updateSSEfalsefalse
append, prepend, before, after, remove, noneAnyfalsefalse
patchAnyfalsefalse

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 for GET requests. 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), and the cache is consumed on click. Links with a non-GET mu-method are excluded from prefetch.

The cache lifetime defaults to 3 seconds. You can adjust it via the prefetchTtl option (in milliseconds):

mu.init({ prefetchTtl: 5000 }); // 5 seconds

Disable globally #

mu.init({ prefetch: false });
<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.

Installation #

Idiomorph is an optional external library. Load it before µJS so that µJS detects it automatically:

<script src="https://cdn.jsdelivr.net/npm/idiomorph@0.7.4/dist/idiomorph.min.js"></script>
<script src="https://unpkg.com/@digicreon/mujs/dist/mu.min.js"></script>
<script>mu.init();</script>

How it works #

When idiomorph is available on the page, µJS uses it for morphing automatically. No configuration needed.

Enable/disable globally #

mu.init({ morph: false });
<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 });
<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. Since µJS applies inline styles for positioning and animation, use !important to override them:

#mu-progress {
    background: red !important;
    height: 5px !important;
    top: auto !important;
    bottom: 0 !important;
}

Custom loading indicator #

For more advanced customization (e.g. a full-screen overlay with a spinner), disable the built-in progress bar and use events to show and hide your own indicator:

mu.init({ progress: false });

document.addEventListener("mu:before-fetch", function() {
    document.getElementById("loading-overlay").style.display = "flex";
});
document.addEventListener("mu:after-render", function() {
    document.getElementById("loading-overlay").style.display = "none";
});
document.addEventListener("mu:fetch-error", function() {
    document.getElementById("loading-overlay").style.display = "none";
});

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-disabledDisable µJS on this element. On links and forms, prevents interception. On <script>, prevents execution during AJAX navigation (see Scripts).
mu-modeInjection mode (replace, update, prepend, append, before, after, remove, none, patch).
mu-targetCSS selector for the target node in the current page. Use & for relative targets.
mu-sourceCSS selector for the source node in the fetched page.
mu-urlOverride the URL to fetch (instead of href / action).
mu-prefixURL prefix for the fetch request.
mu-titleSelector for the title node. Supports selector/attribute syntax. Empty string to disable.
mu-historyAdd URL to browser history (true/false). Default depends on mode and context.
mu-scrollForce (true) or prevent (false) scrolling to top. Default depends on mode and context.
mu-morphDisable morphing on this element (false).
mu-transitionDisable view transitions on this element (false).
mu-prefetchDisable prefetch on hover for this link (false).
mu-methodHTTP method: get, post, put, patch, delete, or sse.
mu-triggerEvent trigger: click, submit, change, blur, focus, load.
mu-debounceDebounce delay in milliseconds (e.g. "500").
mu-repeatPolling interval in milliseconds (e.g. "5000").
mu-confirmShow 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. Use & for relative targets.
mu-patch-mode(Patch fragments) Injection mode for this fragment.
mu-patch-historySet 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
processLinksbooleantrueIntercept link clicks
processFormsbooleantrueIntercept form submissions
targetstring"body"CSS selector for the default replacement target
sourcestring"body"CSS selector for the default content source
titlestring"title"CSS selector for the page title element
modestring"replace"Default injection mode
historybooleantrueAdd URL to browser history
scrollboolean|nullnullScroll behavior. null = auto (depends on mode and context).
urlPrefixstring""Prefix added to all fetch URLs
prefetchbooleantrueEnable hover prefetch
prefetchTtlnumber3000Prefetch cache TTL in milliseconds
morphbooleantrueEnable DOM morphing (when idiomorph is available)
transitionbooleantrueEnable View Transitions API
progressbooleantrueShow progress bar during loading
confirmQuitTextstring"Are you
sure you
want to
leave this
page?"
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
Chrome66+April 2018
Edge79+January 2020
Firefox57+November 2017
Safari12.1+March 2019
Opera53+May 2018

Mobile #

Browser Version Release date
Chrome Android66+April 2018
Safari iOS11.3+March 2018
Firefox Android57+November 2017
Opera Mobile47+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).