Skip to content

Digicreon/muJS

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

µJS (muJS)

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), fetch API, event delegation

Table of contents

Installation

Via <script> tag (recommended)

<script src="/path/to/mu.min.js"></script>
<script>mu.init();</script>

Via CDN

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

Via npm

npm install @digicreon/mujs

Quick start

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

Modes

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

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-ghost="false">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. 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>

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

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

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, allowing the server to distinguish between standard and µJS-initiated requests.

Note: mu-post is deprecated. Use mu-method="post" instead.

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 ghost mode (no browser history entry).

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

Ghost mode

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.

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

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>

DOM morphing

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

View Transitions

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

Progress bar

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>

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.

Example: run code after each page load

document.addEventListener("mu:after-render", function(e) {
    console.log("Loaded: " + e.detail.url);
    myApp.initWidgets();
});

Example: cancel a navigation

document.addEventListener("mu:before-fetch", function(e) {
    if (e.detail.url === "/restricted") {
        e.preventDefault();
    }
});

Example: modify HTML before rendering

document.addEventListener("mu:before-render", function(e) {
    e.detail.html = e.detail.html.replace("foo", "bar");
});

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

Configuration reference

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.

Programmatic API

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

Browser support

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

License

MIT


µJS is developed by Digicreon. Website: mujs.org

About

Lightweight AJAX navigation library

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors