Compose, don't configure
Small focused APIs — signal, computed, on, fromEvent, async. Combine primitives instead of maintaining an overloaded interface.
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.
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.
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.
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).
| Layer | Feature | ElementsKit |
|---|---|---|
| State | Shared reactive state | class Store { @reactive() count = 0 } |
| Derived value | computed(() => store.count * 2) | |
| Server state | Query | async(() => fetch(url())).start() |
| Mutation | mutation.run({ id, name }) | |
| Retry with backoff | async(retry(fn, 5, n => 2**n * 100)).start() | |
| Persist result | async(fetcher.then(v => store(v))).start() | |
| Browser hooks | Online / offline | online() |
| Viewport size | windowSize.width() | |
| Media query | createMediaQuery("(prefers-color-scheme: dark)") | |
| Debounce | createInterval(300) | |
| UI framework | Mount into DOM | render(el, () => <App/>) |
| Function component | function Counter(props) { return <div/> } | |
| Class component | class Counter { render() { return <div/> } } | |
| Lifecycle cleanup | onCleanup(() => …) | |
| Custom elements | Reactive HTMLElement | @attributes class X extends HTMLElement {} |
| Reactive property | @reactive() count = 0 | |
| Attribute handler | static [attr] = { name(this, v) {} } | |
| Slot | children = Slot.new() |
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.
| ElementsKit | Native API | Relationship |
|---|---|---|
| JSX | document.createElement | Compiles to |
promise | Promise | Extends (still awaitable, still chainable) |
async | Promise | Thenable — await asyncInstance works |
| Custom element | HTMLElement | Extends — registered via customElements.define |
@reactive field | Plain class field | Decorator — no proxy, no property rewrite |
on(el, "click", …) | addEventListener | Thin wrapper returning Disposable |
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.
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.
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.
The pieces are independent and composable — not a strict hierarchy. Signals are the shared language that connects everything.
| Pattern | When to use it |
|---|---|
| Plain signals | Pure logic, no UI. Node scripts, workers, utilities that need reactive state. |
| Elements / JSX | Inline reactive UI in scripts. Quick components appended directly to the DOM. |
| Components | Class-based UI with render(). Share state across multiple elements via stores. |
| Custom elements | Reusable across frameworks. Native HTML integration, Shadow DOM, framework-agnostic distribution. |
| Framework integration | Drop 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.
Narrow doesn’t mean incomplete — signals + elements + custom elements cover the full client-side reactive-UI surface.