Skip to content

Philosophy

ElementsKit is a library of reactive primitives, not a framework. Each piece is its own import, runs on its own, and composes with the others — inside React, inside a custom element, or on its own in a script.

Principles

Four threads run through every API choice:

Compose, don't configure

Small focused APIs — signal, computed, on, fromEvent, async. Combine primitives instead of maintaining an overloaded interface.

Close to the platform

Thin or absent abstraction — no virtual DOM, no proxies, no build steps. JSX is createElement, custom elements are HTMLElement.

Predictable and explicit

signal / compose are reactive; nothing else is. No heuristic dependency tracking, no hidden subscriptions.

Designed for the AI age

Primitives compose into higher-level blocks. Swap one block at a time instead of maintaining long lines of code.

Compose, don’t configure

ElementsKit gives you primitives you combine, not a system you configure. No config object, no plugin tree, no convention to memorize — the call site is the contract. Need a query layer? Compose async + retry + storage — swap the fetcher; leave the retry and storage alone. Need browser-API hooks? Import the utility. The surface grows by adding primitives, not by stacking flags on an existing one — overloaded interfaces accumulate breaking changes and deprecations that every consumer has to track and migrate through.

Loading diagram…

The table below maps ElementsKit equivalents to each major feature from the usual stack — a UI framework (React, Vue, Solid), a custom-element library (Lit), a server-state library (TanStack Query, SWR), a browser-hooks pack (react-use, vueuse, solid-primitives), and a state manager (Zustand, Jotai, MobX).

LayerFeatureElementsKit
StateShared reactive stateclass Store { @reactive() count = 0 }
Derived valuecomputed(() => store.count * 2)
Server stateQueryasync(() => fetch(url())).start()
Mutationmutation.run({ id, name })
Retry with backoffasync(retry(fn, 5, n => 2**n * 100)).start()
Persist resultasync(fetcher.then(v => store(v))).start()
Browser hooksOnline / offlineonline()
Viewport sizewindowSize.width()
Media querycreateMediaQuery("(prefers-color-scheme: dark)")
DebouncecreateInterval(300)
UI frameworkMount into DOMrender(el, () => <App/>)
Function componentfunction Counter(props) { return <div/> }
Class componentclass Counter { render() { return <div/> } }
Lifecycle cleanuponCleanup(() => …)
Custom elementsReactive HTMLElement@attributes class X extends HTMLElement {}
Reactive property@reactive() count = 0
Attribute handlerstatic [attr] = { name(this, v) {} }
Slotchildren = Slot.new()

Close to the platform

Heavy abstraction over native APIs is where beginners get burned — they learn the abstraction, not the platform underneath, and the leak surfaces in production. ElementsKit keeps the layer thin or absent. JSX compiles to document.createElement. promise extends the native Promise (still awaitable, still chainable). async instances are thenable. Custom elements are HTMLElement.

ElementsKitNative APIRelationship
JSXdocument.createElementCompiles to
promisePromiseExtends (still awaitable, still chainable)
asyncPromiseThenable — await asyncInstance works
Custom elementHTMLElementExtends — registered via customElements.define
@reactive fieldPlain class fieldDecorator — no proxy, no property rewrite
on(el, "click", …)addEventListenerThin wrapper returning Disposable

Predictable and explicit

No magic. signal / compose are reactive; nothing else is. No heuristic dependency tracking, no hidden subscriptions. Derivations live in computed(...), cleanup in onCleanup. Stores are plain classes with @reactive field decorators.

Designed for the AI age

Writing code is cheap — an agent will draft most of it. Maintaining it is the bottleneck. A codebase is read and rewritten long after it’s first authored, often by an agent rebuilding context from scratch.

ElementsKit is built from primitives that compose into higher-level blocks. Each unit stays testable in isolation, which lets you — or an agent — swap one block at a time without touching the rest.

Framework integration

Any signal can drive a UI-framework component. The hooks are the bridge, not the primitive — the same utilities work inside React today, Solid or Vue when those integrations ship.

Two hooks bridge signals into React: useSignal subscribes a component to any signal or computed; useScope ties effect, onCleanup, and factory utilities to the component’s lifetime for automatic disposal. See React integration for the full treatment.

Architecture

The pieces are independent and composable — not a strict hierarchy. Signals are the shared language that connects everything.

Loading diagram…

Pattern guide

PatternWhen to use it
Plain signalsPure logic, no UI. Node scripts, workers, utilities that need reactive state.
Elements / JSXInline reactive UI in scripts. Quick components appended directly to the DOM.
ComponentsClass-based UI with render(). Share state across multiple elements via stores.
Custom elementsReusable across frameworks. Native HTML integration, Shadow DOM, framework-agnostic distribution.
Framework integrationDrop a signal, store, or utility into an existing React (today; Solid/Vue planned) app via useSignal / useScope. Share reactive state with code that lives outside the framework.

Start with the simplest option that fits — you can always combine patterns.

Not for

Narrow doesn’t mean incomplete — signals + elements + custom elements cover the full client-side reactive-UI surface.

See also