Small reactive components for existing HTML.
Lume lets you attach reactive TypeScript components to existing HTML — no renderer, no Virtual DOM, no JSX. It is calm, clean, and browser-native.
| Format | Size | Gzipped |
|---|---|---|
| ESM (minified) | ~2.4 kB | ~1 kB |
| ESM | ~5.2 kB | ~1.5 kB |
| CJS | ~6.5 kB | ~1.9 kB |
npm install @beardcoder/lume
# or
bun add @beardcoder/lumeHTML:
<div data-lume="disclosure" data-lume-id="main-menu">
<button data-lume-part="button" aria-expanded="false">Toggle</button>
<div data-lume-part="panel">Content</div>
</div>TypeScript:
import { createLume, defineComponent } from "@beardcoder/lume";
const disclosure = defineComponent(({ part, signal, on, effect }) => {
const button = part("button");
const panel = part("panel");
const open = signal(false);
on(button, "click", () => open.update(v => !v));
effect(() => {
panel.hidden = !open();
button.setAttribute("aria-expanded", String(open()));
});
return {
show: () => open.set(true),
hide: () => open.set(false),
};
});
const app = createLume();
app.component("disclosure", disclosure).mount();
const menu = app.require("main-menu");
menu.hide();| Attribute | Purpose |
|---|---|
data-lume |
Marks the root element of a component |
data-lume-part |
Marks a named element or template inside |
data-lume-id |
Assigns an ID so the component API can be retrieved |
Define a component using defineComponent. It receives a context object and returns its public API.
import { defineComponent } from "@beardcoder/lume";
export default defineComponent(({ part, signal, effect, on }) => {
// ...
return { /* public API */ };
});Register and mount:
import { createLume } from "@beardcoder/lume";
import myComponent from "./my-component";
const app = createLume();
app.component("my-component", myComponent).mount();| Property | Description |
|---|---|
root |
The component root element |
part(name) |
Finds one [data-lume-part="name"] inside root |
parts(name) |
Finds all matching parts |
template(name) |
Returns a factory that clones a <template> part |
signal(initial) |
Creates a reactive signal |
computed(fn) |
Creates a derived signal |
effect(fn) |
Runs fn immediately and again when signals change |
untrack(fn) |
Reads signals without subscribing the effect |
on(target, event, handler, options?) |
Adds an event listener with auto cleanup |
cleanup(fn) |
Registers a cleanup function |
emit(name, detail?) |
Emits a local app event |
listen(name, handler) |
Listens to a local app event |
global.emit(name, detail?) |
Emits a global event (shared across apps) |
global.listen(name, handler) |
Listens to a global event |
const count = signal(0); // create
count(); // read (subscribes the surrounding effect)
count.peek(); // read without subscribing
count.set(1); // write
count.update(v => v + 1); // transform
effect(() => {
console.log("count is", count()); // runs on change
});
// Read multiple signals untracked
import { untrack } from "@beardcoder/lume";
const total = untrack(() => price() * quantity());// Inside a component
emit("my-event", { value: 42 });
listen("my-event", (detail) => console.log(detail));
// On the app
app.emit("my-event", { value: 42 });
app.listen("my-event", (detail) => console.log(detail));// Inside a component
global.emit("global-event", payload);
global.listen("global-event", handler);Use native <template> elements with data-lume-part:
<div data-lume="toast" data-lume-id="toaster">
<template data-lume-part="item">
<div class="toast" role="status">
<span data-lume-part="message"></span>
<button type="button">Close</button>
</div>
</template>
</div>const createItem = template("item");
const frag = createItem(); // cloned DocumentFragment
root.appendChild(frag);Expose your component's public methods by returning an object from defineComponent. Access it via app.get(id) or app.require(id).
const menu = app.require<MenuApi>("main-menu");
menu.hide();app.get(id)— returnsT | undefinedapp.require(id)— returnsTor throws a clear error
A plugin is just a function that receives the app:
function myPlugin(app) {
app.component("tooltip", tooltip);
}
app.use(myPlugin);Lume intentionally has no:
- Virtual DOM or renderer
- JSX or custom templates
- Directives (
data-lume-show,data-lume-on, etc.) - Router, store, or SSR
- Animation APIs
- Devtools or inspect
- Framework-style contracts or aliases
HTML marks structure. TypeScript contains the logic.
MIT