
mador.js is an ultra-light reactive DOM runtime that synchronizes plain JavaScript state with existing HTML markup. It updates elements using standard CSS selectors, bypassing Virtual DOM diffing, build steps, and custom template syntax. Ideal for multi-page applications, static sites, and server-rendered templates that need dynamic counters, form previews, or toggle states.
The entire runtime weighs roughly 855 bytes minified and ships as a native ES module. It structures reactivity around two core functions: a read utility that connects CSS selectors to state properties, and a write utility that batches property mutations before applying changes to the document.
Features
- Reactive bindings between state and existing DOM selectors.
- Automatic dependency tracking at the property level.
- Batched property changes inside each
write()call. - Nested plain-object state tracking.
How to Use mador.js
Installation
Install the npm package:
npm install @marsbos/mador
Import it into an ES module:
import mador from "@marsbos/mador";
You can also directly import the module from a CDN:
import mador from "https://cdn.jsdelivr.net/npm/@marsbos/mador/dist/mador.js";
Basic Usage
Define an initial state object, bind it to matching DOM nodes, and trigger mutations on user interaction:
<button id="increment">Increment</button>
<p class="counter"></p>
<p class="triple-counter"></p>
<script type="module">
import mador from "https://cdn.jsdelivr.net/npm/@marsbos/mador/dist/mador.js";
const [read, write] = mador({
count: 1,
});
read(
".counter",
(element, count) => {
element.textContent = `Count: ${count}`;
},
(state) => state.count,
);
read(
".triple-counter",
(element, count) => {
element.textContent = `Triple: ${count * 3}`;
},
(state) => state.count,
);
document.querySelector("#increment").addEventListener("click", () => {
write((state) => {
state.count++;
});
});
</script>
Bind Multiple State Values
Return several values from the state reader when one DOM update depends on more than one property.
const [read, write] = mador({
name: "Ada",
count: 3,
});
read(
".summary",
(element, [name, count]) => {
element.textContent = `${name}: ${count}`;
},
(state) => [state.name, state.count],
);
Public API
Calling mador() returns the two public state functions:
const [read, write] = mador(initialState);
mador(initialState)creates the reactive state store and returns[read, write].read(selector, updateFn, valueFn)registers a DOM binding.valueFnreads the state dependencies, whileupdateFnreceives each matching element and the resulting value.write(updateFn)changes state through the reactive store. Property changes made during one call are processed together.
A read() binding runs once when registered. Later writes rerun it when changed state paths overlap the dependencies recorded by its valueFn.
A property can also receive a function when its next value depends on the previous value:
write((state) => {
state.count = (count) => count + 1;
});
Alternatives & Related Resources
- Minimal State Management for Vanilla JavaScript – Tiny Signals
- Zero-Dependency Reactive DOM from JSON Data – Sprout
FAQs
Q: Can one binding depend on several state properties?
A: Yes. Read each required property inside the third read() callback and return the values needed by the DOM update.
Q: Why does state.items.push() not trigger an update?
A: Arrays are not recursively proxied. Assign a replacement array through write() when array contents change.
Q: What happens after a bound DOM element is removed?
A: On a later relevant state update, a binding with no matching DOM elements is removed from the active runner list.







