mador.js: Tiny Reactive DOM Runtime for Vanilla JS

Category: Javascript | September 7, 2026
Authormarsbos
Last UpdateSeptember 7, 2026
LicenseMIT
Views0 views
mador.js: Tiny Reactive DOM Runtime for Vanilla JS

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. valueFn reads the state dependencies, while updateFn receives 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

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.

You Might Be Interested In:


Leave a Reply