Mador is a tiny reactive DOM runtime that shipped on Hacker News this week under the tagline "Make DOM Reactive. Nothing more." It is aimed squarely at pages that have some HTML and a bit of interactivity but don't want to pull in a framework.
The whole API is one call. mador() takes a plain object and returns a two-element tuple, conventionally destructured as [read, write]:
import mador from "https://cdn.jsdelivr.net/npm/@marsbos/mador@latest/dist/mador.js";
const [read, write] = mador({ count: 1 });
read(
".counter",
(el, count) => {
el.textContent = `Count: ${count}`;
},
(state) => state.count,
);
write((state) => {
state.count++;
});import mador from "https://cdn.jsdelivr.net/npm/@marsbos/mador@latest/dist/mador.js";
const [read, write] = mador({ count: 1 });
read(
".counter",
(el, count) => {
el.textContent = `Count: ${count}`;
},
(state) => state.count,
);
write((state) => {
state.count++;
});That is basically the whole surface. There are no components, no templates, and no virtual DOM. Mador works against the DOM you already have. 1
A binding is three pieces
A reactive binding is one function with three arguments: read(selector, update, read). The first argument selects the elements to update, the second receives each element plus the values it depends on, and the third picks the slice of state the binding cares about.
Mador watches which properties a binding actually touches and only re-runs it when one of those changes:
read(
".counter",
(el, count) => {
el.textContent = count * 2;
},
(state) => state.count,
);read(
".counter",
(el, count) => {
el.textContent = count * 2;
},
(state) => state.count,
);State changes go through write. Writes are batched, so several mutations in a single write are processed together rather than each triggering a pass. 1
Bindings are tied to the DOM they update. When the matching elements disappear, Mador drops the corresponding reactive runner. The author calls out that there is no component lifecycle to manage. 1
The pitch is the size
Two things the author leads with are how small the runtime is (roughly 855 bytes minified 1) and that it needs no build step or global runtime. The Show HN post calls it "a tiny ~80-line reactive state tuple" and something the author "built over the weekend just to experiment with clean, zero-dependency reactivity." 2
The underlying mechanism is a JavaScript Proxy over your state object, with automated dependency tracking and batching in microtasks. It is the same reactive signal idea behind modern reactivity in the bigger libraries. It just ships a stripped-down version of the primitive rather than a view layer.
Where the tradeoffs bite
The HN thread surfaced most of them. The naming drew the first complaint: read is not actually for reading values, which is confusing, and one commenter wished for a single bind call. The author acknowledged the readability issue and planned to tidy it up. 2
Because Mador is a native ES module, it won't run from a file:// page. You need an HTTP server, which rules out distributing a self-contained single HTML file. 2
There was also a recurring "why not just use @preact/signals" line of argument. Signals have been available as standalone, decoupled packages for a while, and one commenter read the README as a false dilemma between Mador and "a framework." The author's counter was that signals typically need .value and per-primitive management, whereas Mador lets you drop in a plain nested object and work with it like normal JS. 2
Proxies carry specific downsides. One commenter noted that a proxy can be confused with its raw value. Add a proxy to a Set and later check for the raw value, or mismatch proxy and raw between a map's keys, and you get surprising behavior. And the author's update detection is reference equality based: an assignment through = triggers the effect, which is worth knowing if you were hoping for deep equality. 2
The verdict
Mador is deliberately not a framework, and it's honest about that. It's the right shape for a page that only needs a little reactivity and where an 855-byte runtime beats managing a full component model. You give up server-rendered escape hatches, a build-tooling ecosystem, and some of the safeguards a framework provides. If the only problem on the page is "some state, some DOM, keep them in sync," that trade is often worth making.
It's early and unproven, but the minimalist direction is quietly appealing. Sometimes you really do just want the DOM reactive, nothing more.
- Mador's README, "mador.js, Make DOM Reactive. Nothing more." https://github.com/marsbos/mador
- "Show HN: Mador, Make any DOM reactive with a tiny 80-line Proxy state tuple," Hacker News. https://news.ycombinator.com/item?id=49590738