
fetch API. The fetched HTML replaces part or all of the current page to prevent full browser refreshes.The library works with any server-side stack (PHP, Python, Go, Ruby), or anything that returns HTML. It’s ideal for developers who want SPA-like navigation speed on a traditionally server-rendered site.
Features:
- Pure vanilla JavaScript with no external requirements. No bundler or compile step needed.
- Update multiple independent DOM fragments from a single server response.
- Full HTTP verb support: GET, POST, PUT, PATCH, and DELETE on links, buttons, and forms.
- Real-time DOM updates through a persistent
EventSourceconnection. - Preserves DOM state (input focus, scroll position, video playback) via optional idiomorph integration.
- Animated page transitions in supported browsers, with a silent fallback on older ones.
- Fetches the target page after a 50ms hover delay for near-instant perceived navigation.
- Custom triggers:
change,blur,focus, orload— with optional debounce and polling intervals. - Top-of-page progress indicator.
- Supports HTML5 validation, file uploads, quit-page confirmation, and custom validator functions.
How To Use It:
1. Download the package and place both script tags at the end of <body>, after your HTML content. µJS scans the DOM on init, so the body must be fully parsed before the call.
<script src="/path/to/dist/mu.min.js"></script> <script>mu.init();</script>
2. After mu.init(), all internal links (URLs beginning with /) are intercepted automatically. µJS fetches the target page and replaces the full <body> by default.
<body>
<nav>
<!-- µJS handles all internal links automatically -->
<a href="/">Home</a>
<a href="/posts">Posts</a>
<a href="/about">About</a>
</nav>
<main id="main-content">
<p>Post list appears here.</p>
</main>
<!-- External URLs are left alone by µJS -->
<a href="https://github.com">GitHub</a>
<!-- Links with target="_blank" are ignored -->
<a href="/archive" target="_blank">Open archive in new tab</a>
<!-- Opt a single link out of µJS interception -->
<a href="/special-page" mu-disabled>Bypass µJS for this link</a>
<script src="/path/to/dist/mu.min.js"></script>
<script>mu.init();</script>
</body>3. To replace only a fragment instead of the full body, set matching mu-target and mu-source selectors:
<!-- Fetch /posts, extract #main-content from the response, and replace the current #main-content with it --> <a href="/posts" mu-target="#main-content" mu-source="#main-content">Posts</a>
4. The mu-mode attribute controls how fetched content lands in the DOM. The default is replace.
replace(string): Replaces the target node with the source node. Default mode.update(string): Replaces the inner HTML of the target with the inner HTML of the source.prepend(string): Inserts the source at the beginning of the target element.append(string): Appends the source at the end of the target element.before(string): Inserts the source immediately before the target in the DOM.after(string): Inserts the source immediately after the target in the DOM.remove(string): Removes the target from the DOM. The source is ignored.none(string): Fires lifecycle events and executes scripts, but makes no DOM changes.patch(string): Processes multiple annotated fragments from the response. See Patch Mode below.
<!-- Refresh only the notification badge count in place --> <a href="/api/notifications/count" mu-mode="update" mu-target="#badge-count" mu-source="#badge-count"> Inbox </a>
5. Patch mode lets one request update multiple independent parts of the page. The server responds with standard HTML elements annotated with mu-patch-target. µJS extracts each annotated element and applies it to the matching DOM node.
Trigger a patch request:
<button mu-url="/api/cart/add/12"
mu-method="post"
mu-mode="patch">
Add to Cart
</button>Server response — multiple independent fragments in one HTML payload:
<!-- Append the new item to the cart list --> <div class="cart-row" mu-patch-target="#cart-items" mu-patch-mode="append"> <span>Wireless Keyboard — $49.99</span> </div> <!-- Update the subtotal display --> <span mu-patch-target="#cart-subtotal">$124.97</span> <!-- Update the header cart badge --> <span mu-patch-target="#cart-badge-count">3</span> <!-- Remove a promo banner that's no longer relevant --> <div mu-patch-target="#promo-banner" mu-patch-mode="remove"></div>
µJS processes each annotated node in order and applies its mu-patch-mode to the corresponding DOM element.
To push the URL to browser history after a patch:
<a href="/products?category=audio" mu-mode="patch" mu-patch-history="true">Audio</a>
6. µJS intercepts form submissions automatically. It runs reportValidity() before sending any request.
<!-- GET form: input data serializes to a query string -->
<form action="/search" method="get"
mu-target="#search-results" mu-source="#search-results">
<input type="text" name="q" placeholder="Search articles...">
<button type="submit">Search</button>
</form>
<!-- POST form: sends as application/x-www-form-urlencoded -->
<form action="/api/newsletter/subscribe" method="post">
<input type="email" name="email" placeholder="your@email.com">
<button type="submit">Subscribe</button>
</form>
<!-- PUT form via mu-method override -->
<form action="/api/user/profile" mu-method="put">
<input type="text" name="display_name" value="Alex">
<button type="submit">Update Profile</button>
</form>
<!-- DELETE form — no body data needed -->
<form action="/api/comment/88" mu-method="delete">
<button type="submit">Delete Comment</button>
</form>
<!-- Warn the user before leaving with unsaved changes -->
<form action="/api/article/draft" method="post" mu-confirm-quit>
<input type="text" name="title" placeholder="Article title">
<textarea name="body"></textarea>
<button type="submit">Save Draft</button>
</form>
Custom validator:
<form action="/api/post/publish" method="post" mu-validate="validatePublishForm">
<input type="text" id="post-title" name="title" placeholder="Post title">
<button type="submit">Publish</button>
</form>
<script>
function validatePublishForm(form) {
// Return true to proceed, false to block the submission
return form.querySelector('#post-title').value.trim().length >= 5;
}
</script>POST form with patch response:
<form action="/api/comment/post" method="post" mu-mode="patch"> <textarea name="body" placeholder="Write a comment..."></textarea> <button type="submit">Post Comment</button> </form>
Server response:
<!-- Append the new comment to the thread --> <div class="comment" mu-patch-target="#comment-thread" mu-patch-mode="append"> <p>Great article, thanks for writing it up.</p> </div> <!-- Replace the form with a fresh blank version --> <form action="/api/comment/post" method="post" mu-patch-target="#comment-form-wrapper"> <textarea name="body" placeholder="Write a comment..."></textarea> <button type="submit">Post Comment</button> </form>
6. Any element with a mu-url attribute can trigger a fetch request. The default trigger for <a> and <button> is click. For <form> it is submit. For <input>, <textarea>, and <select> it is change.
<!-- Live search: fires 400ms after the user stops typing -->
<input type="text" name="q"
mu-trigger="change" mu-debounce="400"
mu-url="/api/search"
mu-target="#search-results" mu-source="#search-results"
mu-mode="update">
<!-- Load city suggestions when the field gains focus -->
<input type="text" name="city"
mu-trigger="focus"
mu-url="/api/city-hints"
mu-target="#city-suggestions" mu-mode="update">
<!-- Auto-save field content when the user tabs away -->
<input type="text" name="page-title"
mu-trigger="blur"
mu-url="/api/draft/autosave" mu-method="put"
mu-target="#autosave-indicator" mu-mode="update">
<!-- Load a content block immediately when the element renders -->
<div mu-trigger="load"
mu-url="/api/recommended-posts"
mu-target="#recommendations" mu-mode="update">
</div>
<!-- Poll for new notifications every 8 seconds -->
<div mu-trigger="load" mu-repeat="8000"
mu-url="/api/notifications/latest"
mu-target="#notification-tray" mu-mode="update">
</div>The polling interval clears automatically when the element leaves the DOM. No manual cleanup needed.
7. Set mu-method="sse" to open a persistent EventSource connection.
<!-- Open an SSE connection on page load and patch the DOM with each message -->
<div mu-trigger="load"
mu-url="/stream/live-activity"
mu-mode="patch"
mu-method="sse">
</div>The server sends HTML fragments, identical in format to patch responses:
event: message data: <li mu-patch-target="#activity-feed" mu-patch-mode="prepend">New signup: alice@example.com</li> event: message data: <span mu-patch-target="#active-user-count">47</span>
µJS closes the EventSource automatically when the element is removed from the DOM.
SSE limitations to keep in mind:
EventSourcedoes not support custom request headers. Pass authentication tokens as query parameters:mu-url="/stream/feed?token=abc123".- HTTP/1.1 browsers cap open SSE connections at six per domain. HTTP/2 removes this limit.
8. All configuration options.
processLinks(bool): Intercept<a>tag clicks. Default:true.processForms(bool): Intercept<form>submissions. Default:true.history(bool): Push the URL to browser history on each navigation. Default:true.mode(string): Default injection mode for all requests. Default:"replace".target(string): Default CSS selector for the DOM node to update. Default:"body".source(string): Default CSS selector for the node to extract from the fetched page. Default:"body".title(string): CSS selector for the element used to update the page title. Supports"selector/attribute"syntax. Default:"title".scroll(bool|null): Scroll-to-top behavior after render.nullmeans auto based on mode and context. Default:null.urlPrefix(string|null): Prefix prepended to all fetched URLs. Default:null.progress(bool): Show the built-in progress bar during requests. Default:true.prefetch(bool): Prefetch target pages on link hover. Default:true.prefetchTtl(number): Prefetch cache expiry in milliseconds. Default:3000.morph(bool): Use idiomorph or a registered custom function for DOM updates. Default:true.transition(bool): Use the View Transitions API when the browser supports it. Default:true.confirmQuitText(string): Text shown in the unsaved-changes confirmation dialog. Default:"Are you sure you want to leave this page?".
mu.init({
target: "#main-content",
source: "#main-content",
progress: true,
prefetch: true,
morph: true
});9. All attributes accept both mu-* and data-mu-* formats.
mu-disabled(boolean): Opts this element out of µJS processing entirely.mu-mode(string): Injection mode for this element. Overrides the global default.mu-target(string): CSS selector of the DOM node to update.mu-source(string): CSS selector of the node to extract from the fetched HTML.mu-url(string): URL to fetch. Overrideshreforactionon the element.mu-prefix(string): URL prefix applied to this specific request.mu-title(string): Selector for the element used to update the page title.mu-history(bool): Push the URL to browser history for this navigation. Overrides global setting.mu-scroll(bool): Scroll to the top after rendering for this navigation.mu-morph(bool): Set"false"to skip morphing for this specific element.mu-transition(bool): Set"false"to skip the View Transition for this navigation.mu-prefetch(bool): Set"false"to disable hover prefetch for this link.mu-method(string): HTTP method for this element:get,post,put,patch,delete, orsse.mu-trigger(string): Event that fires the request:click,submit,change,blur,focus, orload.mu-debounce(number): Debounce delay in milliseconds before the request fires.mu-repeat(number): Polling interval in milliseconds after the firstloadtrigger.mu-confirm(string): Show a browser confirmation dialog before sending the request.mu-confirm-quit(boolean): Prompt the user before leaving if the form has unsaved changes.mu-validate(string): Name of a global JavaScript function to call before form submission.mu-patch-target(string): On patch fragments — the CSS selector of the target node in the current page.mu-patch-mode(string): On patch fragments — injection mode for this fragment.mu-patch-history(bool): Set"true"to push the URL to browser history after a patch. Default:"false".
10. API methods.
// Load a URL programmatically with optional config overrides.
// This bypasses the click/submit event path.
mu.load("/dashboard", { history: false, target: "#workspace", mode: "update" });
// Get the URL of the last page µJS navigated to.
// Returns null if no navigation has occurred yet.
const current = mu.getLastUrl(); // "/dashboard"
// Get the URL of the page before the current one.
const previous = mu.getPreviousUrl(); // "/home"
// Block navigation and page unload when the user has unsaved changes.
mu.setConfirmQuit(true);
// Remove the navigation block (e.g., after a successful form save).
mu.setConfirmQuit(false);
// Register a custom DOM morphing function.
// The function receives (targetNode, htmlString, options).
mu.setMorph(function(target, html, opts) {
MyDiffLib.morph(target, html, opts);
});11. Events.
// Fires after mu.init() completes. Not cancelable.
document.addEventListener("mu:init", function(e) {
console.log("µJS initialized. Starting URL:", e.detail.lastUrl);
});
// Fires before any fetch request is sent.
// Call e.preventDefault() to abort the navigation.
document.addEventListener("mu:before-fetch", function(e) {
if (e.detail.url.startsWith("/admin") && !userIsAdmin) {
e.preventDefault();
window.location.href = "/login";
}
});
// Fires after the fetch, before the DOM is updated.
// Modify e.detail.html to transform the response before injection.
document.addEventListener("mu:before-render", function(e) {
// Replace a server-side placeholder with a client-side value
e.detail.html = e.detail.html.replace("%%BUILD%%", APP_BUILD_ID);
});
// Fires after the DOM has been updated. Use this to re-initialize
// any third-party widgets that the new content requires.
document.addEventListener("mu:after-render", function(e) {
console.log("Rendered:", e.detail.finalUrl);
Prism.highlightAll();
initTooltips();
});
// Fires on network failure or a non-2xx HTTP response.
document.addEventListener("mu:fetch-error", function(e) {
if (e.detail.status === 404) {
document.querySelector("#main-content").innerHTML =
"<p>That page doesn't exist.</p>";
}
if (e.detail.status === 500) {
console.error("Server error at:", e.detail.url);
}
});12. Morphing preserves input focus, textarea content, scroll positions, video playback state, and CSS transitions across page updates. This makes µJS viable for pages with rich interactive components that a raw innerHTML swap would reset.
<script src="/path/to/dist/idiomorph.min.js"></script> <script src="/path/to/dist/mu.min.js"></script> <script>mu.init();</script>
13. The built-in progress bar uses the ID #mu-progress. Override it with CSS:
/* Red, slightly taller progress bar */
#mu-progress {
background: #e63946 !important;
height: 4px !important;
}Alternatives
- Turbo (Hotwire): The server-rendered SPA framework from 37signals.
- HTMX: More powerful for complex interaction patterns, but significantly larger (~14 KB gzipped).
The post Lightweight AJAX Page Navigation Library – µJS appeared first on CSS Script.
Discover more from RSS Feeds Cloud
Subscribe to get the latest posts sent to your email.
