The atom protocol
Two methods — get() and sub() — are the whole contract. Anything that speaks it binds: a Mode Atom, a Mode Data cell, your own object.
µ(*)The reactive DOM engine of the Microdom standard. One function reads, writes
and binds the DOM; any object with get/sub keeps it live.
7.4 KB, zero dependencies, no build step.
the whole idea
µ("#price", { text: 42 }) // writes once
µ("#price", { text: priceAtom }) // keeps writing
That second line is the whole 2.0. µ already knew how to read and write the DOM — now it knows how to listen. Same syntax, zero new API: reactivity is a property of the dispatcher, not a framework bolted on top.
Two methods — get() and sub() — are the whole contract. Anything that speaks it binds: a Mode Atom, a Mode Data cell, your own object.
Bindings touch only the node that changed. No virtual DOM, no render cycles, no wrappers — re-binding releases the old subscription, leak-free.
valueForm controls bind both ways: atom to control, input to atom. Checkboxes and radios speak boolean.
The unit of state — atom, computed, effect. Zero DOM, 1.1 KB. @microdom/mode/atom
Keyed rendering with lifecycle: nodes keep their identity — focus survives, animations finish, reorders move instead of recreating. @microdom/mode/list
A columnar store for high-frequency feeds — triple diff, per-cell notifications, and every cell is a bindable, writable atom. @microdom/mode/data
Try each piece live: binding · keyed list · live data.
The reference case: a live table of 500 instruments — per-cell reactive prices, four percentage columns, a two-way user ranking and buy-at-price — where the store processes each tick in ~1 ms (1000 rows: 1.7 ms; 2000: 3.2 ms). The whole stack for it — grammar, state, live data — is 10.9 KB.
live-table.js
import µ from "@microdom/mode"
import { computed } from "@microdom/mode/atom"
import DataStore from "@microdom/mode/data"
const store = new DataStore("coin_id")
const price = store.cell(id, "price") // a cell IS an atom
µ(cell, { text: computed(() => fmt(price.get()), [price]) })
price.set(42000) // the DOM follows
µ doesn't wrap the DOM — it phrases it. Every output is a plain native element or NodeList: no wrapper objects, no library residue mounted on nodes, no re-implemented events. µ abstracts the phrasing of your code, never the objects it hands you. That's why Mode coexists with any other library, and why leaving it costs nothing: your DOM was native all along.
The same synthesis that shortens the code shortens the execution. What you don't write, the browser doesn't run: no virtual DOM to diff, no render cycles to schedule, no wrappers to allocate. Granular bindings touch only the cell that changed. And a good default is state you never have to compute — a decision already made, sitting there, free. Readability, weight and runtime economy are not three virtues: they are one synthesis, seen from three sides.
Zero dependencies. Mode.js ships with no runtime dependencies — the whole core is a single 7.4 KB file.
terminal
npm install @microdom/mode
Each entry point ships in three formats — pick the one that matches how you load it.
| File | Format | Load with |
|---|---|---|
dist/mode.js | ESM | import (bundlers, <script type="module">) |
dist/mode.esm.min.js | ESM, minified | import for the smallest module build |
dist/mode.min.js | IIFE | classic <script> — exposes the global µ |
The same three formats exist for atom, list,
data and move. An IIFE build has no exports, so it can't be
imported — use ESM for import, IIFE for plain <script>.
Resolves to the ESM build:
app.js
import µ from "@microdom/mode"
import "@microdom/mode/list" // the import IS the installation
import "@microdom/mode/move" // import the core before any extension
Global µ, no build step:
index.html
<script src="https://cdn.jsdelivr.net/gh/microdom/mode.js@v2.0.0/dist/mode.min.js"></script>
<script>
µ("body", { /* ... */ })
</script>
index.html
<script type="module">
import µ from "https://cdn.jsdelivr.net/gh/microdom/mode.js@v2.0.0/dist/mode.js"
µ("body", { /* ... */ })
</script>
Ships with 2.0 · optional import
Animation lives in a separate, optional module, leaving the 7.4 KB core untouched. Import it and Mode gains slide / fade / animate / show / hide commands plus the offset / pos getters — all chained through the same µ(...) call.
move.js
µ("#panel", { slideUp: { t: 300 } })
µ("#panel", { fadeIn: { t: 200, fn: () => console.log("done") } })
µ("#box", { animate: { props: { width: 400 }, t: 500, easing: "easeOut" } })
µ("#box", { stop: {} }) // cancel running animation
const off = µ("#box", { offset: null }) // → { top, left }
If an interaction cannot be expressed simply, the abstraction is probably wrong.