A tiny functional JavaScript UI library for AI agents to generate lightweight, interactive micro-apps.
- Lightweight — zero dependencies, no build step, no virtual DOM overhead.
- Simple state management — use plain variables or the built-in
storefor shared state. - Smooth updates — changes are batched and applied efficiently, no flicker or jank.
- Form-friendly — inputs, video, canvas, focus, and scroll position all survive re-renders.
- Stable lists — reorder, add, or remove items without losing element state.
- Dynamic attributes — bind classes, styles, and props with simple template expressions.
- Secure by default — user content is auto-escaped to prevent injection attacks.
- Fault tolerant — one broken component won't crash the rest of your app.
- AI-ready — minimal API surface that agents can generate without a toolchain.
pnpm add @opentf/micro-ui
# or
npm i @opentf/micro-ui
# or
yarn add @opentf/micro-ui
# or
bun add @opentf/micro-uiimport {
define,
html,
store,
update,
flush,
mount,
onReady,
onError,
} from "@opentf/micro-ui";<script type="module">
import { define, html, update } from "https://esm.sh/@opentf/micro-ui";
define("x-counter", (el, props) => {
let count = Number(props.count || 0);
return () => html`
<button onclick=${() => { count++; update(el); }}>
Count: ${count}
</button>
`;
});
</script>
<x-counter count="0"></x-counter>CDN alternatives:
// esm.sh
import { define, html, update } from "https://esm.sh/@opentf/micro-ui";
// esm.run (via jsDelivr)
import { define, html, update } from "https://esm.run/@opentf/micro-ui";
// unpkg
import { define, html, update } from "https://esm.unpkg.com/@opentf/micro-ui";define("x-demo", (el, props) => {
let data = [];
// Post-render setup & cleanup
onReady(() => {
const interval = setInterval(() => update(el), 1000);
return () => clearInterval(interval); // cleanup on disconnect
});
// Error handling
onError((target, err, phase) => {
console.error(`Failed in ${phase}:`, err);
});
return () => html`<p>${data.length} items</p>`;
});define("x-counter", (el) => {
let count = 0;
return () => html`
<button onclick=${() => { count++; update(el); }}>
${count}
</button>
`;
});define("x-child", (el, props) => {
return () => html`<span>${props.name}</span>`;
});
define("x-parent", (el) => {
let name = "World";
return () => html`<x-child name=${name}></x-child>`;
});Registers a custom element. setup(el, props) runs once on connect and must return a render function.
define("x-greeting", (el, props) => {
let name = props.name || "World";
return () => html`<h2>Hello, ${name}</h2>`;
});Tagged template that produces a renderable tree. Supports text, attributes, events, keyed lists, conditionals, and nesting. Text is escaped by default.
html`<button onclick=${handler} class="btn ${active}">Click</button>`Trusted HTML opt-in — bypasses escaping. Never pass user input.
html.raw`<div>${trustedMarkup}</div>`Triggers a re-render. Multiple calls are batched into a single update. No-op on errored components.
Immediately processes pending updates synchronously.
update(el);
flush(); // DOM is now up to dateClears a host element and appends a new Custom Element. Returns the child.
const app = mount(document.getElementById("app"), "x-app");Runs after initial render. Return a function for cleanup on disconnect. Call inside setup.
define("x-widget", (el) => {
onReady(() => {
return () => console.log("cleaned up");
});
return () => html`<p>Hello</p>`;
});Registers a callback when setup, render, or reconcile throws. Call inside setup.
define("x-widget", (el) => {
onError((target, err, phase) => {
console.error(`Error in ${phase}:`, err);
});
return () => html`<p>Hello</p>`;
});Simple key-value store. Get with store.get, set with store.set. Does not trigger DOM updates — connect to components via store.subscribe.
store.set("counter", 0);
store.get("counter"); // 0
store.set("counter", 1);Set a nested value by dot-separated path. Immuntably clones the object tree along the path.
store.set("form", { name: "", email: "" });
store.set("form", "Ada", { path: "name" });
store.get("form", { path: "name" }); // "Ada"Listen for changes to a store key. Returns an unsubscribe function. Call inside onReady to connect store changes to component re-renders.
define("x-counter", (el) => {
onReady(() => store.subscribe("counter", () => update(el)));
return () => html`<span>${store.get("counter")}</span>`;
});Delete a store key (resets to undefined) or remove a nested key from an object value. Notifies subscribers.
store.del("counter"); // full key deleted
store.del("form", { path: "name" }); // only removes "name", rest intactAll ${value} text interpolations are escaped automatically. The five standard HTML-significant characters are converted to entities:
| Character | Entity |
|---|---|
& |
& |
< |
< |
> |
> |
" |
" |
' |
' |
This means user-supplied content is safe to render directly:
const userInput = '<script>alert("xss")</script>';
html`<p>${userInput}</p>` // renders as literal text, not markupWhen you need to render trusted markup, use html.raw:
html.raw`<b>${boldText}</b>` // parsed as real HTMLhtml.raw still stringifies interpolated values, but the entire result is treated as markup and parsed via innerHTML. Never pass user-controlled input to html.raw.
A throwing setup, render, or reconcile is caught and isolated per-component:
- The failed element is replaced with a small inline error box (
<div data-micro-ui-error>). - The instance is marked
errored— furtherupdate()calls are no-ops. - The host page and all sibling components continue running normally.
- Recovery requires removing the element from the DOM and creating a fresh one.
define("x-safe", (el) => {
onError((target, err, phase) => {
console.error(`Error in ${phase}:`, err);
});
return () => {
if (Math.random() > 0.5) throw new Error("boom");
return html`<p>Works</p>`;
};
});- State: ordinary JavaScript closures. Optional built-in
store/subscribe/delfor shared state — still no signals or reactive primitives. - Composition: native Custom Elements.
<x-parent>contains<x-child>as regular HTML. - DOM identity: elements, inputs, videos, canvases survive updates. No
innerHTMLrebuilds. - Reconciliation: positional by default; keyed when
keypresent (key=${id}) — stable DOM reuse for cart / data lists. Matches old and new children by index or key. Unchanged nodes are reused. Changed nodes are patched in place.
- Not a React/Vue/Angular replacement for large apps
- Not optimized (v0 uses a full internal tree for correctness)
- Not a build tool or compiler
- No SSR/hydration — client islands only; updates are batched via
queueMicrotask
MIT
