lume-js
v2.3.1
Published
Minimal reactive state management using only standard JavaScript and HTML - no custom syntax, no build step required
Maintainers
Readme
Why Lume.js?
| Feature | Lume.js | Alpine.js | Vue | React |
|---------|---------|-----------|-----|-------|
| Custom Syntax | ❌ No | ✅ x-data | ✅ v-bind | ✅ JSX |
| Build Step | ❌ Optional | ❌ Optional | ⚠️ Recommended | ✅ Required |
| Bundle Size | 1.46–2.66KB | ~15KB | ~35KB | ~45KB |
| HTML Validation | ✅ Pass | ⚠️ Warnings | ⚠️ Warnings | ❌ JSX |
| Extensible Handlers | ✅ | ❌ Built-in only | ❌ Built-in only | N/A |
Lume.js is "Modern Knockout.js" — standards-only reactivity for the modern web.
Installation
Pick your entry
| Entry | Size (gz) | Contents | For |
|-------|-----------|----------|-----|
| lume-js/state | 1.46 KB | state, batch, withReadObserver | Node, Deno, Bun, workers, CLI — anywhere without a DOM |
| lume-js | 2.66 KB | + bindDom, effect | Browsers |
| lume-js/addons | pay per import | computed, watch, repeat, persist, … | Optional patterns |
| lume-js/handlers | pay per import | show, classToggle, on, … | Extra reactive attributes |
Via CDN (Recommended for simple projects)
<script type="module">
import { state, bindDom, effect } from 'https://cdn.jsdelivr.net/npm/lume-js/dist/index.min.mjs';
</script>DOM-free kernel only (servers, workers, embedded):
<script type="module">
import { state, batch } from 'https://cdn.jsdelivr.net/npm/lume-js/dist/state.min.mjs';
</script>Via NPM (Recommended for bundlers)
npm install lume-jsimport { state, bindDom } from 'lume-js'; // browser: full core
import { state, batch } from 'lume-js/state'; // Node/CLI/workers: 1.46 KB kernel→ Using Lume without a DOM? See the Universal core guide.
Browser Support
| Browser | Minimum version | |---------|-----------------| | Chrome | 80+ | | Firefox | 74+ | | Safari | 13.1+ | | Edge | 80+ | | IE11 | ❌ Not supported |
The floor comes from optional chaining / nullish coalescing (ES2020) used in the source — shipped un-transpiled, true to no-build. IE11 cannot be polyfilled regardless: Lume uses Proxy.
Quick Start
HTML:
<div>
<h1>Hello, <span data-bind="name"></span>!</h1>
<input data-bind="name" placeholder="Enter your name">
</div>JavaScript:
import { state, bindDom } from 'lume-js';
const store = state({ name: 'World' });
bindDom(document.body, store);That's it — two-way binding, no build step, valid HTML.
Lists? Also just HTML.
<ul id="todos">
<template>
<li><span data-bind="title"></span> <em data-bind="$index"></em></li>
</template>
</ul>import { repeat } from 'lume-js/addons';
repeat('#todos', store, 'todos', { key: t => t.id, template: true });The row structure is a standard <template> element — no createElement, no JSX, no custom syntax.
Multiple stores? Batch them.
import { batch } from 'lume-js';
batch(() => {
cart.items = [...cart.items, item];
totals.sum += item.price;
ui.message = 'Added!';
});
// effects depending on several stores ran exactly ONCE, synchronouslyBuilt-in Reactive Attributes
bindDom() supports these data-* attributes out of the box:
<!-- Two-way binding (inputs) / one-way (text elements) -->
<input data-bind="name">
<span data-bind="name"></span>
<!-- Boolean attributes -->
<div data-hidden="isLoading">Content</div>
<button data-disabled="isSubmitting">Submit</button>
<input data-checked="isAgreed" type="checkbox">
<input data-required="fieldRequired">
<!-- ARIA attributes -->
<button data-aria-expanded="menuOpen">Menu</button>
<div data-aria-hidden="isCollapsed">Panel</div>Extensible Handler System
Handlers are plain objects that teach bindDom() how to interpret new data-* attributes. They live in lume-js/handlers and are entirely optional — import only the ones you use. You can also write your own with just an attr string and an apply function.
Need more reactive attributes? Import handlers or create your own — no core modification needed.
import { state, bindDom } from 'lume-js';
import { show, classToggle, stringAttr } from 'lume-js/handlers';
const store = state({
isVisible: true,
isActive: false,
profileUrl: '/user/alice'
});
bindDom(document.body, store, {
handlers: [show, classToggle('active'), stringAttr('href')]
});<span data-show="isVisible">Visible when truthy</span>
<div data-class-active="isActive">Toggles 'active' class</div>
<a data-href="profileUrl">Profile</a>Available Handlers (lume-js/handlers)
| Handler | HTML Example | Effect |
|---------|-------------|--------|
| show | data-show="key" | Shows element when truthy (el.hidden = !val) |
| className | data-classname="key" | Replaces full class string (el.className = val) |
| boolAttr(name) | data-readonly="key" | Toggles any boolean attribute |
| ariaAttr(name) | data-aria-pressed="key" | Sets ARIA attribute to "true"/"false" |
| classToggle(...names) | data-class-active="key" | Toggles individual CSS classes |
| stringAttr(name) | data-href="key" | Sets string attributes (removes on null) |
| on(...types) | data-onclick="key" | Wires the function at that state key as an event listener |
| htmlAttrs() | (all of the above) | One-import preset — all standard HTML + ARIA attrs |
Presets
import { formHandlers, a11yHandlers } from 'lume-js/handlers';
// formHandlers: [boolAttr('readonly')]
// a11yHandlers: [ariaAttr('pressed'), ariaAttr('selected'), ariaAttr('disabled')]Custom Handlers
Any plain object with attr and apply works:
const tooltip = {
attr: 'data-tooltip',
apply(el, val) { el.title = val ?? ''; }
};
bindDom(root, store, { handlers: [tooltip] });Addons
Addons are optional reactive pattern helpers that build on the core primitives. They handle common use cases that would otherwise require boilerplate — derived values, key observation, list rendering. Import only what you need from lume-js/addons; none are loaded by default.
import { computed, watch, repeat } from 'lume-js/addons';| Addon | When to use |
|-------|-------------|
| effect(fn) (core) | Write derived values back into the store, or trigger side effects on state change |
| computed(fn) | Derive a read-only value from state to consume outside the store (templates, display logic) |
| watch(store, key, fn) | React to a specific key changing — DOM updates, analytics, syncing external state |
| repeat(container, store, key, opts) | Render a keyed list with element reuse — incl. declarative template: mode bound straight from a <template> element |
| persist(store, key, opts) | Sync selected keys with localStorage/sessionStorage — hydrate on call, auto-save on change |
| createCleanupGroup() | Collect multiple cleanup/unsubscribe functions and dispose them all at once |
| hydrateState(selector?, validate?) | Read initial state from a <script type="application/json"> tag (SSR hydration), with optional schema validation |
Quick rule: effect for writing back into state → computed for reading outside state → watch for observing a single key → repeat for arrays in the DOM.
effect() vs computed() vs watch()
import { state, effect } from 'lume-js';
import { computed, watch } from 'lume-js/addons';
const store = state({ firstName: 'Ada', lastName: 'Lovelace', count: 0 });
// effect() — derives a value and writes it back into the store
// Use when the result lives in state and drives the DOM via data-bind
effect(() => {
store.fullName = `${store.firstName} ${store.lastName}`;
});
// computed() — derives a value to read externally (e.g. display, logging)
// Use when the result is consumed outside the store
const doubled = computed(() => store.count * 2);
console.log(doubled.value); // 10
doubled.subscribe(val => document.title = `Count × 2: ${val}`);
// watch() — reacts to a single key changing
// Use for side effects tied to one property: analytics, localStorage, DOM sync
watch(store, 'count', (val) => {
localStorage.setItem('count', val);
});
// watch() with { immediate: false } — skip the initial call
watch(store, 'count', (val) => {
sendAnalytics('count_changed', val); // only on actual changes
}, { immediate: false });Using Lume.js with AI agents
Lume ships agent-ready documentation inside the npm package, regenerated from the docs on every change (CI-enforced), so it always matches the installed version:
| File | What it is |
|------|-----------|
| AGENT_GUIDE.md | Distilled rules, pitfalls, and canonical patterns — one read teaches an agent to write correct Lume code |
| llms.txt | llmstxt.org index of all docs, for web-browsing agents |
| llms-full.txt | Every guide, tutorial, and API page in one file |
In a project that uses Lume, add one line to your agent config (CLAUDE.md, AGENTS.md, .cursorrules, …):
Before writing any code that uses lume-js, read node_modules/lume-js/AGENT_GUIDE.md.
For API details beyond the guide, consult node_modules/lume-js/llms-full.txt.No install? Point the agent at the hosted copies: https://raw.githubusercontent.com/sathvikc/lume-js/main/AGENT_GUIDE.md and …/llms-full.txt.
Documentation
Browse the full docs at sathvikc.github.io/lume-js, or read the source markdown in the docs/ directory:
- Tutorials
- API Reference
- Guides
- Universal core (Node, CLI, workers) — using
lume-js/statewithout a DOM - Choosing reactive primitives — when to use effect vs computed vs watch
- Cleanup & Disposal — tearing down effects, bindings, and subscriptions
- SSR & Hydration — server-rendered HTML with reactive hydration
- Universal core (Node, CLI, workers) — using
- Design
Contributing
We welcome contributions! Please read CONTRIBUTING.md for details.
License
MIT © Sathvik C
