
Reactivity that follows web standards.
Minimal reactive state management using only standard JavaScript and HTML.
No custom syntax · No build step · No framework lock-in.
npm install lume-js
| Feature | Lume.js | Alpine.js | Vue | React |
|---|---|---|---|---|
| Custom Syntax | ❌ No | ✅ x-data |
✅ v-bind |
✅ JSX |
| Build Step | ❌ Optional | ❌ Optional | ✅ Required | |
| Bundle Size | ~2.23KB | ~15KB | ~35KB | ~45KB |
| HTML Validation | ✅ Pass | ❌ JSX | ||
| Extensible Handlers | ✅ | ❌ Built-in only | ❌ Built-in only | N/A |
Lume.js is "Modern Knockout.js" — standards-only reactivity for the modern web.
<script type="module">
import { state, bindDom, effect } from 'https://cdn.jsdelivr.net/npm/lume-js/dist/index.min.mjs';
</script>npm install lume-jsimport { state, bindDom } from 'lume-js';| Browser | Minimum version |
|---|---|
| Chrome | 49+ |
| Firefox | 18+ |
| Safari | 10+ |
| Edge | 79+ |
| IE11 | ❌ Not supported |
IE11 cannot be polyfilled — Lume uses Proxy.
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.
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>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>| 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) |
htmlAttrs() |
(all of the above) | One-import preset — all standard HTML + ARIA attrs |
import { formHandlers, a11yHandlers } from 'lume-js/handlers';
// formHandlers: [boolAttr('readonly')]
// a11yHandlers: [ariaAttr('pressed'), ariaAttr('selected'), ariaAttr('disabled')]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 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 (no full re-render on change) |
createCleanupGroup() |
Collect multiple cleanup/unsubscribe functions and dispose them all at once |
hydrateState(selector?) |
Read initial state from a <script type="application/json"> tag (SSR hydration) |
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.
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 });Full documentation is available in the docs/ directory:
- Tutorials
- API Reference
- Guides
- 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
- Design
We welcome contributions! Please read CONTRIBUTING.md for details.
MIT © Sathvik C