Show HN: µJS, a 5KB alternative to Htmx and Turbo with zero dependencies (mujs.org)

161 points by amaury_bouchard ↗ HN
I built µJS because I wanted AJAX navigation without the verbosity of HTMX or the overhead of Turbo.

It intercepts links and form submissions, fetches pages via AJAX, and swaps fragments of the DOM. Single <script> tag, one call to `mu.init()`. No build step, no dependencies.

Key features: patch mode (update multiple fragments in one request), SSE support, DOM morphing via idiomorph, View Transitions, prefetch on hover, polling, and full HTTP verb support on any element.

At ~5KB gzipped, it's smaller than HTMX (16KB) and Turbo (25KB), and works with any backend: PHP, Python, Go, Ruby, whatever.

Playground: https://mujs.org/playground

Comparison with HTMX and Turbo: https://mujs.org/comparison

About the project creation, why and when: https://mujs.org/about

GitHub: https://github.com/Digicreon/muJS

Happy to discuss the project.

40 comments

[ 2.5 ms ] story [ 57.9 ms ] thread
Would love to see a comparison with Datastar too
Reminds me a little of htmz

htmz is a minimalist HTML microframework for creating interactive and modular web user interfaces with the familiar simplicity of plain HTML.

quite cool, but the browser history being messed up drives me bonkers. is that easy to fix?
Yes, htmz is a great project! The core idea is similar, but htmz is intentionally more minimal (it's literally 166 bytes). µJS covers more ground: patch mode, SSE, DOM morphing, View Transitions, prefetch, polling, full HTTP verb support.
Does it automatically parse JSON responses from servers into objects? This is my one big gripe about htmx, even though the devs and other users keep telling me I shouldn't want that as a feature and that it "doesn't make sense".

Sorry if I need to use existing APIs I cannot change.

  Browser -> your server route -> server calls API -> server renders HTML -> htmx swaps it?
They're right through, the entire point of HTMX is to use "HTML as the Engine of Application State". Shipping JSON to the front end to be rednered is antithetical to that.

As someone else mentioned, having your own server backend act as an intermediary between your front end and the API that serves JSON is probably the most straightforward solution to keep everything HTMX-y.

µJS is HTML-over-the-wire, so it doesn't parse JSON responses natively (same limitation as HTMX). That said, you can work around it using the `mu:before-render` event, which lets you intercept the raw response before it hits the DOM and handle it however you want. It's not as clean as native JSON support, I'll admit. If your use case is primarily consuming existing JSON APIs, µJS (and HTMX) are probably not the right fit, they're designed around server-rendered HTML fragments.
> the devs and other users keep telling me I shouldn't want that as a feature and that it "doesn't make sense"

I mean if the devs and the users are all telling you it doesn't make sense, maybe it really doesn't make sense. Not everything is supposed to be able to do everything. Some things are targeted and focused on specific use cases.

I like the idea. DOM morphing is nice.

I've done this previously with morphdom to AJAXify a purely server-driven backoffice system in a company.

I would love something even smaller. No `mu-` attributes (just rely on `id`, `href`, `rel`, `rev` and standard HTML semantics).

There's a nice `resource` attribute in RDFa which makes a lot of sense for these kinds of things: https://www.w3.org/TR/rdfa-lite/#h-resource

Overall, I think old 2015-era microdata like RDFa and this approach would work very well. Instead of reinventing attributes, using a standard.

Thanks for the feedback! The RDFa `resource` attribute is an interesting angle I hadn't considered. That said, µJS's default behavior actually requires zero `mu-` attributes; it intercepts standard `href` links and `action` forms out of the box. The `mu-` attributes are only needed when you want to override the default behavior, use patch mode (updating multiple DOM fragments in a single request), or trigger requests from elements that are not links or forms. So in the simplest case, a single `mu.init()` call is all you need. That said, if you want something even more minimal, htmz takes that philosophy to the extreme.
There’s several other (well) known examples of the use of mujs.

There’s Artifex’s interpreter from muPDF. It’s also the basis of several JS related projects: https://mujs.com/

There’s also a lesser known interpreter: https://github.com/ccxvii/mujs

And IIRC, there was a CommonJS library of the same name.

Thanks for the list! Yes, the name is used by several projects. Mine is the browser navigation one — hopefully the "µJS" name, the .org domain and the use case make it distinct enough.
Not to be confused with https://mujs.com/ I guess?
Indeed! While the names are similar, µJS (mujs.org) and MuJS (mujs.com) are completely different projects. The former is a browser navigation library (a lightweight alternative to HTMX and Turbo), while the latter is an embeddable JavaScript interpreter written in C, developed by Artifex for MuPDF. Sorry for the confusion!
Looks useful! I skimmed through the docs and had a question.

Is there a mechanism for loading HTML partials that require additional style or script file? And possibly a way to trigger a JS action when loaded? For example, loading an image gallery.

Yes! `<script>` and `<style>` tags found in the fetched content are processed on the fly, as long as they haven't been loaded already. So you can include a gallery-specific stylesheet or script directly in the partial, and µJS will inject and execute them automatically. For triggering a JS action after rendering, the `mu:after-render` event is the right hook.
It's about time browsers start supporting something like this natively. Fingers crossed.

I'll be checking this out. Any chance you (or anyone) has had a run with this lib + web components? I'd love to hear about it.

Nice project, always interesting to see HTMX-inspired frameworks.

If you want something even more minimalistic, I did Swap.js: 100 lines of code, handles AJAX navigation, browser history, custom listeners when parts of DOM are swapped, etc.

https://github.com/josephernest/Swap.js

Using it for a few production products and it works quite well!

I don't do a lot of frontend work, but I'm glad to see more of this stuff popping up. Anything that combats the normal framework insanity is a good thing in my book. And this looks like a really cool idea. The default routing approach is an awesome idea, swapping the entire body by default is also really interesting. It seemed odd to me at first, but the more I thought about it the more it made sense.
What's the best way to handle having multiple frontend components respond to events from a single SSE connection from some parent node data provider? Ideally we do not what many SSE connections within a page for each component.. right? Then what's the best practice to handle that single SSE handler on the server and frontend? If that makes sense...
I'm curious to understand, why would you build your website this way vs. say jQuery. I've never really understood the HTMX ecosystem. Is this just to avoid javascript and replace that with html pages, id's and attributes? It feels like the DOM is a very clear abstraction and scripting is a more powerful way to manipulate it. What do people like or prefer about this approach and paradigm?
Hey, cool library had just a small nitpick/request wrt https://mujs.org/playground

Could you please add all sources as tabs? For example in Form (GET) I would really like to see /demos/search-results.html and the same goes for other examples.

Thanks!

Good news, I've just added the server-side source as a tab in the Playground. Thanks again for the suggestion!
Layman’s question: How to avoid the page jumping back to the top in the Playground examples?
I really like these sorts of frameworks from an architectural perspective, but what's the use-case? Maybe I'm too SPA-pilled, because to me all the fun of Web development is in providing really fluid, skeuomorphic experiences like those enabled by, eg pragmatic-drag-and-drop[0] or yjs[1].

I just struggle to envision what application benefits from the efficiency that this or htmx offer, but from neither the ultra-interactive, nor the ultra-collaborative. Maybe updating stock ticker prices? Fast-service back-of-house ticketing displays?

I would love to feel called to reach for this library.

0. https://github.com/atlassian/pragmatic-drag-and-drop

1. https://github.com/yjs/yjs

At ~5KB gzipped this is impressively small. What were the biggest features you deliberately left out to keep the size that low?
Another library ignores shadow dom.

As of now you don't need any frameworks for this. The new command and navigation apis, would just do the same trick.

Quite impressive! I’ve been looking into Turbo alternatives so it showed up on my feed the right time.

One gripe with the name though is that I’m used to uTorrent’s and uws’s use of “u” for the “µ” character. So, my first guess would be to look for uJS to find this project, not muJS.

I’m curious: why does the default behavior do the magic replacement for absolute paths links but not for relative paths?

Also, perhaps the CDN script snippets in the getting started page should include the integrity attribute.

Both points are now addressed: relative paths are handled, and the documentation includes the integrity hash. Thanks for taking the time to suggest these improvements!
I tried this out today and it doesn't seem to work for me, because it only considers relative urls to be "internal." If the urls are full urls (https://domain:port/...) then it won't work. It seems like it should be able to use window.location to work out that these full urls are also "internal" as well.
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.
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.

"feel like a single-page app" except that its not and it will never be. I hate HTML-over-the-wire solutions to any problem. There is not a worse solution that has been more normalized than html-over-the-wire.