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.

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
});

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
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.

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:

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, 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. 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-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.
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.
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
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).