A web-native, build-free reactive UI library.
- No build step: write modern ESM and run it directly in the browser.
- Vanilla JS: no compiler macros, no JSX transform.
- Fine-grained DOM updates (no VDOM diff).
- Signals + derived signals for reactive state.
- Inline derivations (
signal.derive(...)) and inline list mappings (arrayObserver.map(...)). - HTML tagged template literals via
html. - Deterministic teardown for rendered entities (listeners cleaned up on
destroy).
Import aliases can be handled with HTML import maps:
<script type="importmap">
{
"imports": {
"jet": "./src/index.js"
}
}
</script>NOTE: This repository is best viewed as a research and reference implementation of Jet. It documents a number of architectural ideas and experiments that informed the project, but it is not a complete framework release and certain areas remain intentionally underdeveloped or in flux. These docs and code should be considered experimental and subject to change.
- Jet is suitable for prototypes and internal tools today.
- Expect breaking changes while the public API settles.
Jet intentionally does not revolve around a “component” abstraction, and therefore does not provide a conventional component lifecycle API (mount, update, unmount, hooks, etc.).
Instead:
- You build UI from renderable specs (
htmltemplates,element(...)specs, signal bindings, array mappings). - Rendering creates internal entities that own DOM ranges and subscriptions.
- Teardown is driven by entity destruction (
destroy) rather than component unmount hooks.
Practically, this means you generally don’t write code that depends on a component instance existing. You write:
- plain functions that produce specs
- signals/observers for state
- event handlers (
config.on) for DOM events and (where applicable) entity lifecycle events
Jet is ESM-first.
import {
render,
element,
html,
fallback,
include,
tokens,
observe,
observeArray,
observeObject,
createFilter,
compute
} from 'jet'import { render, element, html, observe, observeArray } from 'jet'
const count = observe(0)
const doubled = count.derive(n => n * 2)
const items = observeArray(['a', 'b'])
const incrementButton = element(
'button',
{
on: {
click: () => {
count.value = count.value + 1
}
}
},
html`Count: ${count}`
)
const app = html`
<main>
${incrementButton}
<p>Doubled: ${doubled}</p>
<ul>
${items.map((item) => html`<li>${item}</li>`)}
</ul>
</main>
`
render(document.body, {}, app)html is a tagged template function that produces a template spec Jet knows how to render.
In templates, interpolations can be:
- primitives (
string,number,boolean,null,undefined) - other templates (
html) - element specs from
element(...) - signals / derived signals from
observe(...)/.derive(...) - array mapping specs from
observeArray(...).map(...) - promises (rendered via a fallback controller)
Renders into an existing DOM element (for example document.body).
rootElementis an actual DOM element (used as the root host).contentcan be anything Jet can render (template, primitive, signal, mapping, etc.).
Creates an element spec.
config supports common buckets:
attrs: attributesprops: propertiesstyle: style propertieson: event listeners / lifecycle events
For hyphen-separated attribute names, you can use nested object syntax. For example, data-theme="dark" can be expressed as:
import { element, html } from 'jet'
const panel = element('div', {
attrs: {
data: {
theme: 'dark'
}
}
}, html`Hello`)tokens is a tagged template helper for building space-separated token lists (commonly class strings). It:
- supports nested token lists
- supports arrays
- supports signals and derived signals inside the template (it unwraps them and subscribes)
It’s designed to be used in attrs, props, or style through Jet’s IDL managers.
import { element, html, tokens, observe } from 'jet'
const primary = observe(true)
const variant = primary.derive(v => v ? 'btn-primary' : 'btn-secondary')
const button = element('button', {
attrs: {
class: tokens`btn ${variant}`
}
}, html`Click me`)Creates a “fallback controller” spec.
- Jet renders
fallbackContentimmediately. - Once the promise resolves, Jet replaces that region with the resolved content.
Loads an external resource and returns a promise that resolves to content.
.jsis loaded via dynamicimport().- everything else is loaded via
fetch()and rendered as anhtmltemplate. - results are cached by path.
Creates a signal.
- Read the current value:
signal.value - Write a new value:
signal.value = next - Listen for changes:
signal.on('change', handler)
Derivations are inline and local.
const count = observe(1)
const label = count.derive(n => `Count = ${n}`)Jet tracks derived signals using WeakRef + FinalizationRegistry so that derived signals can be garbage-collected if you drop all strong references to them.
In other words: creating derived signals does not inherently require you to manually unregister them.
More precisely:
- Signals are normal JS objects: if nothing references them anymore, the runtime can collect them.
- Derived signals are registered against their parent using weak references, so the parent signal does not keep derived signals alive.
- If a derived signal is still referenced (or still subscribed-to by some live rendering), it will not be collected.
observeArray provides a proxy-like array value that captures mutations and emits change operations.
- Mutating methods like
push,splice,unshift, etc. are wrapped. - Changes are queued and flushed on a microtask.
observeArray(...).map(...) returns an array mapping spec. When interpolated into a template, Jet renders a region that updates in response to array operations.
Internally, Jet coalesces array DOM updates and schedules flushes with requestAnimationFrame for smoothness under rapid updates.
Additional useful bits:
clone({ sync: true })can create a follower array observer that applies operations from the source.- Filtering hooks exist and can drive a
filter.changeevent during mapping.
observeObject wraps a plain object in a proxy that emits operations for:
- setting properties
- deleting properties
- replacing the entire object
It also includes ergonomic helpers:
obj.set(key, value)obj.assign(partial)obj.update(fn)obj.replace(next)
Jet templates are authored as:
html`<div class="panel">${/* ... */''}</div>`For syntax highlighting and basic IntelliSense inside html\...``, install a lit-html compatible extension.
- Recommended: the VS Code extension “lit-plugin”
- Alternative: an extension that provides lit-html highlighting
Most of these extensions recognize the tag name html by default, or allow configuration to treat html tagged templates as HTML.
- Don’t call
render()inside a template. Jet treatsrender()as a root-only operation. - Signals and rendered entities are event targets; teardown is designed to be deterministic (listeners removed after
destroy).
MIT