Thanks for the report! This is now fixed in v1.4.4.
µJS now resolves URLs using the native URL constructor and compares origin against window.location.origin. This means all same-origin URLs are correctly recognized as internal:
- Absolute URLs: https://domain:port/page
- Relative URLs: ../page.html, page.html
- Local paths: /page (already worked)
Hash-only links (#section) are intentionally left to the browser for native anchor scrolling.
Thanks for the report! Using fully absolute URLs for internal links is not a very common pattern, and this hadn't been raised before. That said, it's a valid and interesting use case, I'll add it to the roadmap. In the meantime, using root-relative URLs (starting with /) is the recommended approach.
Fair enough, it's clearly not the right tool for everyone. HTML-over-the-wire works well for server-rendered apps where the primary interaction is fetching and displaying content. For highly interactive UIs with complex client-side state, a proper SPA framework is the better choice. Different problems, different tools.
That said, the vast majority of websites are purely transactional: click a link, load a page; submit a form, load a page. For those, there's little reason to add a full frontend framework. HTML-over-the-wire can improve responsiveness without adding complexity.
idk. I'm just extremely not sold on the idea of having practically any interaction going over the wire unless absolutely necessary. Keeping everything in the browser for as long as is physically possible is what improves responsiveness, a single page application, once loaded, is not susceptible to spotty connections, dropping packets, a slow connection, whatever, like html-over-the-wire is.
I have been extremely bearish on html-over-the-wire solutions from the minute I saw and understood what HTMX was trying to achieve. In my eyes, the only way to truly achieve what I, and users, expect in a web page is with a SPA. I understand the hesitation to use heavier SPAs, but my hesitation to fetching html from the server after the page load to update the page is much larger. But I also do understand how html-over-the-wire provides a good middle ground between web 1.0 apps where basically every interaction reloaded the entire page, and web 2.0 apps that feel closer to an actual application rather than a website.
On relative paths: the current behavior is intentional for simplicity. µJS checks whether the URL starts with a slash (but not a double slash) to identify internal links. No one has reported this as an issue so far, but it's a valid feature request and I'll keep it in mind for a future version.
On the integrity attribute: the reason it was missing is that the library was evolving quickly and the hash would have changed with every release. Now that it's stable, I'll add it.
That's a valid use case. For opt-in per link, you can disable global link interception with `mu.init({ processLinks: false })` and then add `mu-url` explicitly to the links you want µJS to handle. For the variable base path scenario, the `urlPrefix` option in `mu.init()` might also help. It prepends a prefix to all fetched URLs.
That said, proper support for relative paths is on the roadmap.
Thanks! The short answer: µJS doesn't try to be a DSL in HTML attributes.
Here's what I deliberately left out compared to Turbo (~25KB gzip) and htmx (~16KB gzip):
- No client-side template/extension system. htmx has a full extension API, header-based control flow (HX-Trigger, HX-Redirect, HX-Reswap...), and dozens of response headers the server can use to command the client. µJS has 5 custom headers total; the server returns HTML, the client renders it.
- No CSS selector-based targeting in attributes. htmx lets you write hx-target="closest tr", hx-target="find .result", hx-target="next div" with a mini query language. µJS uses plain CSS selectors only (mu-target="#id"), no traversal keywords.
- No built-in transition/animation classes. htmx adds/removes CSS classes during swaps (htmx-settling, htmx-swapping, htmx-added) with configurable timing. Turbo has turbo-preview, transition classes, etc. µJS defers to the native View Transitions API: zero animation code in the library, and it falls back silently.
- No client-side cache or snapshots. Turbo keeps a page cache for "instant" back-button rendering and manages preview snapshots. µJS stores only scroll position in history.state and lets the browser's own cache + fetch() handle the rest.
- Patch mode simpler than Turbo Frames. Turbo has <turbo-frame>, a custom element with lazy-loading, src rewriting, and frame-scoped navigation. µJS handles multi-fragment updates via patch mode (mu-mode="patch") with regular HTML elements. No custom elements, no frame concept. It's simpler but it works perfectly.
- No request queuing or throttling. htmx has hx-sync with queuing strategies (queue, drop, abort, replace). µJS simply aborts the previous in-flight request. One request at a time per navigation.
- No form serialization formats. htmx supports JSON encoding (hx-encoding), nested object serialization, etc. µJS uses the browser's native FormData for POST and URL params for GET. That's it.
- No JavaScript API surface to speak of. htmx exposes htmx.ajax(), htmx.process(), htmx.trigger(), event details, etc. µJS exposes mu.init(), mu.fetch(), mu.render(), and a few setters and a few events. The goal is that you shouldn't need the JS API — the HTML attributes should be enough.
What I kept: the features that cover 90% of real-world use cases. AJAX navigation, 8 injection modes, multi-fragment patch, morphing (via Idiomorph), SSE, prefetch, forms with all HTTP verbs, triggers with debounce/polling, progress bar, history/scroll management. Just without the layers of abstraction around them.
The philosophy: if the browser already does it (View Transitions, FormData, fetch, AbortController, history API), don't reimplement it.
Fair point on shadow DOM. That said, µJS targets server-rendered HTML workflows, where pages are plain HTML generated by a backend framework or template engine. In that context, shadow DOM is rarely used, so this limitation doesn't affect the typical µJS user. If you're building heavily component-based UIs with Web Components, µJS is probably not the right tool anyway. A proper JS framework would serve you better.
On the native APIs: yes, the Navigation API and Speculation Rules are exciting, but browser support is still uneven. µJS works today across all modern browsers without any configuration. That said, I agree the gap will narrow over time.
No worries! You can prevent the page from scrolling to the top by adding `mu-scroll="false"` to the link or form triggering the request. You can also disable it globally with `mu.init({ scroll: false })`.
In the Playground, the scroll-to-top behavior is intentiona, it's there to illustrate that feature.
µJS now resolves URLs using the native URL constructor and compares origin against window.location.origin. This means all same-origin URLs are correctly recognized as internal: - Absolute URLs: https://domain:port/page - Relative URLs: ../page.html, page.html - Local paths: /page (already worked)
Hash-only links (#section) are intentionally left to the browser for native anchor scrolling.
reply