MicroDOM ecosystem
Stable v2.0.0 Zero dependencies

Mode JS µ(*)

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.

Install Docs Playground View on GitHub

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.

What ships in 2.0

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.

Granular binding

Bindings touch only the node that changed. No virtual DOM, no render cycles, no wrappers — re-binding releases the old subscription, leak-free.

Two-way value

Form controls bind both ways: atom to control, input to atom. Checkboxes and radios speak boolean.

Mode Atom

The unit of state — atom, computed, effect. Zero DOM, 1.1 KB. @microdom/mode/atom

Mode List

Keyed rendering with lifecycle: nodes keep their identity — focus survives, animations finish, reorders move instead of recreating. @microdom/mode/list

Mode Data

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.

One language, measured at scale

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

Doctrine

µ 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.

Install

Zero dependencies. Mode.js ships with no runtime dependencies — the whole core is a single 7.4 KB file.

terminal

npm install @microdom/mode

Builds

Each entry point ships in three formats — pick the one that matches how you load it.

FileFormatLoad with
dist/mode.jsESMimport (bundlers, <script type="module">)
dist/mode.esm.min.jsESM, minifiedimport for the smallest module build
dist/mode.min.jsIIFEclassic <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>.

Usage

Bundler / npm

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

Browser — classic script

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>

Browser — native ES module

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

Mode Move — animation through the same call

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.

Get it on npm See the whole ecosystem