Skip to content

Introduction

ElementsKit is a toolkit of reactive primitives β€” signals, JSX, custom elements, and browser-API helpers. Import one at a time, compose them, or use any of them inside a UI framework (React, etc.).

  • Compose, don’t configure. Small focused APIs β€” signal, computed, on, fromEvent, async. Combine primitives instead of maintaining an overloaded interface.

  • Close to the platform. JSX compiles to document.createElement. promise extends Promise. Custom elements are HTMLElement. Thin or absent abstraction layers β€” no virtual DOM, no proxies, no build steps.

  • Predictable and explicit β€” no magic. signal/compose are reactive; nothing else is. No heuristic dependency tracking, no hidden subscriptions.

  • Designed for the AI age. Code is cheap; maintenance still isn’t. Primitives compose into higher-level blocks. Swap one block at a time instead of maintaining long lines of code.

  • Bundler-friendly. Every primitive is its own subpath β€” elements-kit/signals, elements-kit/utilities/media-query, elements-kit/integrations/react. Import only what you need.

Signals & Stores

Fine-grained reactive state. signal, computed, effect, and @reactive class fields β€” plain classes, no proxies. Shared across custom elements, React components, and plain scripts, all in sync.

Utilities

Browser-API signals β€” online, windowSize, createMediaQuery, on, fromEvent, observers, and more. Reactive wrappers, pay-per-import.

Async

async, promise, retry, createLocalStorage. Compose queries, mutations, persistence, and revalidation β€” no query library required.

Elements

JSX compiles to real document.createElement calls. Reactive props become live DOM bindings β€” no diffing, no reconciliation, no runtime overhead.

Custom Elements

Native HTMLElement subclasses enhanced with signals, JSX, and decorators. Usable in any HTML context β€” React, Vue, or plain HTML β€” no adapters needed.

Framework Integration

Bridge any signal into a UI framework via useSignal / useScope. React today, Svelte and Vue planned.

Install

Terminal window
pnpm add elements-kit

Full setup β€” TypeScript JSX config, CDN, Deno β€” in Installation.

import {
function signal<T>(): Updater<T> & Computed<T> (+1 overload)

Creates a mutable reactive signal.

  • Read: call with no arguments β†’ returns the current value and subscribes the active tracking context.
  • Write: call with a value β†’ updates the signal and schedules downstream effects if the value changed.

@example

const count = signal(0);
count(); // β†’ 0 (read)
count(1); // write – effects depending on count will re-run
count(); // β†’ 1

signal
,
function computed<T>(getter: (previousValue?: T) => T): () => T

Creates a lazily-evaluated computed value.

The getter is only called when the computed value is read and one of its dependencies has changed since the last evaluation. If nothing has changed the cached value is returned without re-running getter.

Computed values are read-only; they cannot be set directly.

@param ― getter - Pure function deriving a value from other reactive sources. Receives the previous value as an optional optimisation hint.

@example

const a = signal(1);
const b = signal(2);
const sum = computed(() => a() + b());
sum(); // β†’ 3
a(10);
sum(); // β†’ 12 (re-evaluated lazily)

computed
} from "elements-kit/signals";
import {
function render(target: Element | DocumentFragment, setup: () => Node | null | undefined): () => void

Mount a node into target with a scoped lifetime.

setup runs inside a detached effectScope. The returned node is appended to target. Calling the returned unmount removes the node from the DOM, disposes its Symbol.dispose hook (JSX-created elements carry one), and tears down every effect / onCleanup registered inside setup.

@example

import { render } from "elements-kit/render";
const unmount = render(document.getElementById("app")!, () => <App />);
// later
unmount();

render
} from "elements-kit/render";
import type {
type ReactiveProps<P> = { readonly [K in keyof P]: Computed<P[K]>; } & {
readonly [RAW_PROPS]?: P;
}
ReactiveProps
} from "elements-kit/jsx-runtime";
function
function Counter(props: ReactiveProps<{
initial?: number;
}>): JSX$1.Element
Counter
(
props: ReactiveProps<{
initial?: number;
}>
props
:
type ReactiveProps<P> = { readonly [K in keyof P]: Computed<P[K]>; } & {
readonly [RAW_PROPS]?: P;
}
ReactiveProps
<{
initial?: number | undefined
initial
?: number }>) {
const
const count: Updater<number> & Computed<number>
count
=
signal<number>(initialValue: number): Updater<number> & Computed<number> (+1 overload)

Creates a mutable reactive signal.

  • Read: call with no arguments β†’ returns the current value and subscribes the active tracking context.
  • Write: call with a value β†’ updates the signal and schedules downstream effects if the value changed.

@example

const count = signal(0);
count(); // β†’ 0 (read)
count(1); // write – effects depending on count will re-run
count(); // β†’ 1

signal
(
props: ReactiveProps<{
initial?: number;
}>
props
.
initial?: Computed<number | undefined> | undefined
initial
() ?? 0);
const
const doubled: () => number
doubled
=
computed<number>(getter: (previousValue?: number | undefined) => number): () => number

Creates a lazily-evaluated computed value.

The getter is only called when the computed value is read and one of its dependencies has changed since the last evaluation. If nothing has changed the cached value is returned without re-running getter.

Computed values are read-only; they cannot be set directly.

@param ― getter - Pure function deriving a value from other reactive sources. Receives the previous value as an optional optimisation hint.

@example

const a = signal(1);
const b = signal(2);
const sum = computed(() => a() + b());
sum(); // β†’ 3
a(10);
sum(); // β†’ 12 (re-evaluated lazily)

computed
(() =>
const count: () => number (+1 overload)
count
() * 2);
return (
<section>
<
p: WithJsxNamespaces<JSX.HTMLAttributes<HTMLParagraphElement>>
p
><strong>{
const count: Updater<number> & Computed<number>
count
}</strong> Γ— 2 = <strong>{
const doubled: () => number
doubled
}</strong></
p: WithJsxNamespaces<JSX.HTMLAttributes<HTMLParagraphElement>>
p
>
<
button: WithJsxNamespaces<JSX.ButtonHTMLAttributes<HTMLButtonElement>>
button
JSX.CustomEventHandlersNamespaced<HTMLButtonElement>["on:click"]?: JSX.EventHandlerWithOptionsUnion<HTMLButtonElement, MouseEvent, JSX.EventHandler<HTMLButtonElement, MouseEvent>> | undefined
on
:
JSX.CustomEventHandlersNamespaced<HTMLButtonElement>["on:click"]?: JSX.EventHandlerWithOptionsUnion<HTMLButtonElement, MouseEvent, JSX.EventHandler<HTMLButtonElement, MouseEvent>> | undefined
click
={() =>
const count: (value: number) => void (+1 overload)
count
(
const count: () => number (+1 overload)
count
() + 1)}>+1</
button: WithJsxNamespaces<JSX.ButtonHTMLAttributes<HTMLButtonElement>>
button
>{" "}
<
button: WithJsxNamespaces<JSX.ButtonHTMLAttributes<HTMLButtonElement>>
button
JSX.CustomEventHandlersNamespaced<HTMLButtonElement>["on:click"]?: JSX.EventHandlerWithOptionsUnion<HTMLButtonElement, MouseEvent, JSX.EventHandler<HTMLButtonElement, MouseEvent>> | undefined
on
:
JSX.CustomEventHandlersNamespaced<HTMLButtonElement>["on:click"]?: JSX.EventHandlerWithOptionsUnion<HTMLButtonElement, MouseEvent, JSX.EventHandler<HTMLButtonElement, MouseEvent>> | undefined
click
={() =>
const count: (value: number) => void (+1 overload)
count
(
const count: () => number (+1 overload)
count
() - 1)}>βˆ’1</
button: WithJsxNamespaces<JSX.ButtonHTMLAttributes<HTMLButtonElement>>
button
>
</section>
);
}
function render(target: Element | DocumentFragment, setup: () => Node | null | undefined): () => void

Mount a node into target with a scoped lifetime.

setup runs inside a detached effectScope. The returned node is appended to target. Calling the returned unmount removes the node from the DOM, disposes its Symbol.dispose hook (JSX-created elements carry one), and tears down every effect / onCleanup registered inside setup.

@example

import { render } from "elements-kit/render";
const unmount = render(document.getElementById("app")!, () => <App />);
// later
unmount();

render
(
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
Document.getElementById(elementId: string): HTMLElement | null

The getElementById() method of the Document interface returns an Element object representing the element whose id property matches the specified string. Since element IDs are required to be unique if specified, they're a useful way to get access to a specific element quickly.

getElementById
("app")!, () => <
function Counter(props: ReactiveProps<{
initial?: number;
}>): JSX$1.Element
Counter
initial?: MaybeReactive<number> | undefined
initial
={0} />);

See the full progression β€” signals β†’ element β†’ function component β†’ class β†’ custom element β€” in the Quick start.

Start here

Explore