Reactivity that follows web standards.
Minimal reactive state management using only standard JavaScript and HTML. No custom syntax, no build step required, no framework lock-in.
Current Release: v2.0.1 | Stability Contract: Core API is frozen forever
Install:npm install lume-js
Bundle size: ~2.44KB gzipped | 321 tests passing
| Feature | Lume.js | Alpine.js | Vue | React |
|---|---|---|---|---|
| Custom Syntax | ❌ No | ✅ x-data |
✅ v-bind |
✅ JSX |
| Build Step | ❌ Optional | ❌ Optional | ✅ Required | |
| Bundle Size | ~2.44KB | ~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>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) |
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 CSS classes |
stringAttr(name) |
data-href="key" |
Sets string attributes (removes on null) |
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] });Import only what you need from lume-js/addons:
import { computed, watch, repeat } from 'lume-js/addons';computed(fn)— Cached derived values with auto-trackingwatch(store, key, fn)— Subscribe to state changesrepeat(container, store, key, options)— Keyed list rendering with element reuse
Full documentation is available in the docs/ directory:
- Tutorials
- API Reference
- Design
We welcome contributions! Please read CONTRIBUTING.md for details.
MIT © Sathvik C