Skip to content

sathvikc/lume-js

Repository files navigation

Lume.js

Lume.js

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.

License: MIT   v2.2.1   355 tests   2.23KB

npm install lume-js


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 ~2.23KB ~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

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>

Via NPM (Recommended for bundlers)

npm install lume-js
import { state, bindDom } from 'lume-js';

Browser Support

Browser Minimum version
Chrome 49+
Firefox 18+
Safari 10+
Edge 79+
IE11 ❌ Not supported

IE11 cannot be polyfilled — 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.


Built-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)
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 (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.

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 });

Documentation

Full documentation is available in the docs/ directory:


Contributing

We welcome contributions! Please read CONTRIBUTING.md for details.

License

MIT © Sathvik C

About

Minimal reactive state management using only standard JavaScript and HTML - no custom syntax, no build step required

Resources

License

Contributing

Stars

Watchers

Forks

Contributors