Framework Adapters
•6 min read
Vanilla Adapter
Directive's vanilla adapter gives you three ways to build reactive UIs with zero framework overhead. Create typed DOM elements, bind them to system state, and choose from el() calls, JSX, or htm tagged templates.
Installation
@directive-run/el versions independently from the rest of the Directive ecosystem. Its core (el(), JSX, htm) has zero dependency on @directive-run/core.
# Standalone — no Directive dependency
npm install @directive-run/el
# With reactive bindings (bind, bindText, mount)
npm install @directive-run/el @directive-run/core
# For htm tagged templates (optional)
npm install htm
Three Ways to Write
// 1. el() — function calls, no build step
el("div", { className: "card" }, el("h2", "Title"), el("p", "Body"))
// 2. JSX — familiar syntax, compiles to el() calls
<div className="card"><h2>Title</h2><p>Body</p></div>
// 3. htm — tagged templates, no build step
html`<div className="card"><h2>Title</h2><p>Body</p></div>`
All three produce real DOM nodes. No virtual DOM, no diffing, no reconciliation.
Setup
Create your system in a shared file:
// system.ts
import { createModule, createSystem, t } from "@directive-run/core";
const counter = createModule("counter", {
schema: {
facts: { count: t.number() },
derivations: { doubled: t.number() },
events: { increment: {}, decrement: {} },
requirements: {},
},
init: (facts) => {
facts.count = 0;
},
derive: {
doubled: (facts) => facts.count * 2,
},
events: {
increment: (facts) => { facts.count += 1; },
decrement: (facts) => { facts.count -= 1; },
},
});
export const system = createSystem({ module: counter });
system.start();
el() — Element Creation
Create typed DOM elements with optional props and children. Props are auto-detected — if the second argument is a child, the empty {} is not needed.
import { el } from "@directive-run/el";
// Without props — no {} needed
el("p", "Hello world")
el("ul", items.map(item => el("li", item)))
el("div", el("h1", "Title"), el("p", "Body"))
// With props — plain objects detected as props
el("a", { href: "/home", className: "nav" }, "Home")
el("input", { type: "email", value: "a@b.com" })
// Event handlers attached at creation
el("button", { onclick: () => save() }, "Save")
// Conditional children — false/null/undefined silently skipped
el("div", hasError && el("p", { className: "error" }, message))
// Numbers coerce to text nodes
el("span", "Score: ", score)
Type Safety
The return type is inferred from the tag name:
const input = el("input", { type: "email" });
// ^? HTMLInputElement — type, value, checked all auto-complete
const canvas = el("canvas", { width: 800, height: 600 });
// ^? HTMLCanvasElement — getContext() available
Children Types
| Type | Behavior |
|---|---|
string | Creates a text node |
number | Coerces to text node (0 renders as "0") |
Node | Appended directly |
ElChild[] | Flattened recursively |
null, undefined, false, true | Silently skipped |
JSX Runtime
Write JSX that compiles to el() calls. No React required.
Setup
Add to your tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@directive-run/el"
}
}
Usage
// App.tsx
import { system } from "./system";
const app = (
<div className="card">
<h2>Counter</h2>
<p>Count: {system.facts.count}</p>
<button onclick={() => system.dispatch({ type: "increment" })}>+</button>
<button onclick={() => system.dispatch({ type: "decrement" })}>-</button>
{showError && <p className="error">Something went wrong</p>}
<ul>
{items.map(item => <li>{item}</li>)}
</ul>
</div>
);
document.body.appendChild(app);
JSX produces real DOM nodes
Unlike React, this JSX creates actual HTMLElement instances. There is no virtual DOM, no reconciliation, and no component lifecycle. For reactive updates, combine with bind(), bindText(), or mount().
htm (Tagged Templates)
Write HTML-like templates with no build step. Uses htm (700 bytes) bound to el().
Setup
npm install htm
Usage
import { html } from "@directive-run/el/htm";
const app = html`
<div className="card">
<h2>Counter</h2>
<p>Count: ${count}</p>
<button onclick=${() => increment()}>+</button>
<ul>
${items.map(item => html`<li>${item}</li>`)}
</ul>
</div>
`;
document.body.appendChild(app);
Works in plain .js files with no TypeScript or bundler. Great for prototyping, CDN-based projects, or <script type="module"> in HTML files.
Reactive Bindings
bind()
Subscribe an element to a Directive system. The updater runs immediately with current state, then on every change. Returns a cleanup function.
import { el, bind } from "@directive-run/el";
const badge = el("span", { className: "badge" });
const cleanup = bind(system, badge, (el, facts, derived) => {
el.textContent = `${facts.count}`;
el.className = facts.count > 10 ? "badge high" : "badge low";
});
// Later: unsubscribe
cleanup();
bindText()
Shorthand for binding text content:
import { el, bindText } from "@directive-run/el";
const label = el("span");
const cleanup = bindText(system, label, (facts, derived) => {
return `${derived.doubled} items`;
});
mount()
Replace a container's children on every state change. Uses replaceChildren() for a single DOM operation per update. Ideal for lists and conditional rendering.
import { el, mount } from "@directive-run/el";
const listEl = el("ul");
const cleanup = mount(system, listEl, (facts) => {
return facts.items.map(item => el("li", item));
});
The renderer can return a single node or an array:
mount(system, container, (facts) => {
if (facts.loading) {
return el("p", "Loading...");
}
return [
el("h2", "Results"),
el("ul", results.map(r => el("li", r))),
];
});
Multi-Module Systems
For namespaced systems with multiple modules, use system.subscribe() directly instead of bind():
import { el } from "@directive-run/el";
import { createSystem } from "@directive-run/core";
const system = createSystem({
modules: {
rocket: rocketModule,
ship: shipModule,
nav: navModule,
},
});
system.start();
const fuelSpan = el("span");
const hullSpan = el("span");
const distSpan = el("span");
system.subscribe(["rocket.*"], () => {
fuelSpan.textContent = `${Math.round(system.facts.rocket.fuel)}%`;
});
system.subscribe(["ship.*"], () => {
hullSpan.textContent = `${Math.round(system.facts.ship.hull)}%`;
});
system.subscribe(["nav.*"], () => {
distSpan.textContent = `${Math.round(system.facts.nav.distance)} km`;
});
Patterns
Full Page with el()
import { el, bind, bindText, mount } from "@directive-run/el";
import { system } from "./system";
// Build the UI declaratively
const countSpan = el("span");
const listEl = el("ul");
bindText(system, countSpan, (facts) => `${facts.count}`);
mount(system, listEl, (facts) => {
return facts.items.map(item => el("li", item));
});
const app = el("div", { className: "app" },
el("header",
el("h1", "My App"),
el("p", "Count: ", countSpan),
),
el("main",
el("button", { onclick: () => system.dispatch({ type: "increment" }) }, "+"),
el("button", { onclick: () => system.dispatch({ type: "decrement" }) }, "-"),
listEl,
),
);
document.body.appendChild(app);
CDN Usage with htm
<script type="module">
import { createModule, createSystem, t } from "https://esm.sh/@directive-run/core";
import { el } from "https://esm.sh/@directive-run/el";
import htm from "https://esm.sh/htm";
const html = htm.bind(el);
// ... define module and system ...
const app = html`
`;
document.body.appendChild(app);
</script>
Combining with Other Frameworks
el() creates standard DOM nodes, so you can use it alongside any framework:
// Inside a React useEffect
useEffect(() => {
const widget = el("div", { className: "widget" },
el("canvas", { id: "chart", width: 400, height: 200 }),
);
containerRef.current?.appendChild(widget);
return () => widget.remove();
}, []);
Next Steps
- API Reference – Full API documentation
- Quick Start – Build your first module
- Core Concepts – Facts, derivations, and constraints

