Skip to content

Elements

An element is a real DOM node. JSX in ElementsKit compiles directly to document.createElement β€” there is no virtual DOM, no diffing, no reconciliation. Every expression produces an actual node you can hold in a variable, append to the DOM, or pass to a function.

Configure JSX once in tsconfig.json:

{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "elements-kit"
}
}

Or use a per-file pragma instead:

/* @jsxImportSource elements-kit */

JSX = createElement

Every JSX tag is a createElement call. Props become attributes or event listeners, children become appended nodes.

// This JSX:
const el = (
<button class="primary" on:click={() => submit()}>
Save
</button>
);
// Compiles to exactly this:
const el = document.createElement("button");
el.setAttribute("class", "primary");
el.addEventListener("click", () => submit());
el.append("Save");

No framework runtime is involved. The element is a plain HTMLButtonElement the moment the expression evaluates.


Reactive props = effects

When you pass a signal or a () => T function as a prop, ElementsKit creates an effect behind the scenes that keeps the DOM in sync. Every time the signal changes, the effect re-runs and re-assigns the property β€” just like writing it manually with effect.

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 effect(fn: () => void): () => void

Creates a reactive side-effect that runs immediately and re-runs whenever any signal or computed it read during its last execution changes.

Use

onCleanup

inside fn to register teardown logic that runs before each re-execution and on final disposal.

If effect is called inside an effectScope or another effect, the new effect is automatically owned by the outer scope and will be disposed when the scope is disposed.

@param ― fn - The side-effect body. Reactive reads inside this function establish dependency links.

@returns ― A disposal function. Call it to stop the effect and run any registered cleanup.

@example

const url = signal('/api/data');
const stop = effect(() => {
const controller = new AbortController();
fetch(url(), { signal: controller.signal });
onCleanup(() => controller.abort());
});
url('/api/other'); // previous fetch is aborted, new one starts
stop(); // final cleanup: abort the last fetch

effect
} from "elements-kit/signals";
const
const label: Updater<string> & Computed<string>
label
=
signal<string>(initialValue: string): Updater<string> & Computed<string> (+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
("Save");
const
const disabled: Updater<boolean> & Computed<boolean>
disabled
=
signal<boolean>(initialValue: boolean): Updater<boolean> & Computed<boolean> (+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
(false);
// This reactive JSX:
const
const el: JSX$1.Element
el
= <
button: WithJsxNamespaces<JSX.ButtonHTMLAttributes<HTMLButtonElement>>
button
JSX.ButtonHTMLAttributes<HTMLButtonElement>.disabled?: JSX.FunctionMaybe<boolean | undefined>
disabled
={
const disabled: Updater<boolean> & Computed<boolean>
disabled
}>{
const label: Updater<string> & Computed<string>
label
}</
button: WithJsxNamespaces<JSX.ButtonHTMLAttributes<HTMLButtonElement>>
button
>;
// Is equivalent to:
const
const el: HTMLButtonElement
el
=
var document: Document

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

MDN Reference

document
.
Document.createElement<"button">(tagName: "button", options?: ElementCreationOptions): HTMLButtonElement (+2 overloads)

In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.

MDN Reference

createElement
("button");
function effect(fn: () => void): () => void

Creates a reactive side-effect that runs immediately and re-runs whenever any signal or computed it read during its last execution changes.

Use

onCleanup

inside fn to register teardown logic that runs before each re-execution and on final disposal.

If effect is called inside an effectScope or another effect, the new effect is automatically owned by the outer scope and will be disposed when the scope is disposed.

@param ― fn - The side-effect body. Reactive reads inside this function establish dependency links.

@returns ― A disposal function. Call it to stop the effect and run any registered cleanup.

@example

const url = signal('/api/data');
const stop = effect(() => {
const controller = new AbortController();
fetch(url(), { signal: controller.signal });
onCleanup(() => controller.abort());
});
url('/api/other'); // previous fetch is aborted, new one starts
stop(); // final cleanup: abort the last fetch

effect
(() => {
const el: JSX$1.Element
el
.
any
disabled
=
const disabled: () => boolean (+1 overload)
disabled
(); });
function effect(fn: () => void): () => void

Creates a reactive side-effect that runs immediately and re-runs whenever any signal or computed it read during its last execution changes.

Use

onCleanup

inside fn to register teardown logic that runs before each re-execution and on final disposal.

If effect is called inside an effectScope or another effect, the new effect is automatically owned by the outer scope and will be disposed when the scope is disposed.

@param ― fn - The side-effect body. Reactive reads inside this function establish dependency links.

@returns ― A disposal function. Call it to stop the effect and run any registered cleanup.

@example

const url = signal('/api/data');
const stop = effect(() => {
const controller = new AbortController();
fetch(url(), { signal: controller.signal });
onCleanup(() => controller.abort());
});
url('/api/other'); // previous fetch is aborted, new one starts
stop(); // final cleanup: abort the last fetch

effect
(() => { /* text node updated in place */ });
// label signal drives a live text node β€” same principle

Passing a plain value (not a signal or function) sets the prop once and never updates it β€” no effect is created.

// Static β€” set once at render time
<input placeholder="Enter text" maxLength={100} />
// Reactive β€” updates on every change
<input placeholder={hint} maxLength={computed(() => limit())} />

Children

Any of the following are valid element children:

TypeBehaviour
string / numberStatic text node, set once
Signal<T>Live text node β€” updates in place when signal changes
() => TLive thunk β€” re-evaluated on every dep change
Element / NodeAppended directly
null / false / undefinedNothing rendered
ArrayEach item rendered in order
const show = signal(true);
const count = signal(0);
<div>
Static text
{count} // live β€” updates in place
{() => count() * 2} // thunk β€” also live
{() => show() && <strong>Hi</strong>} // conditional element
{[1, 2, 3].map((n) => <li>{n}</li>)} // array of elements
</div>

Prop namespaces

ElementsKit extends standard HTML props with namespaced prefixes for common reactive patterns:

SyntaxEffectEquivalent
on:click={fn}Event listener (case-preserving)el.addEventListener("click", fn)
class:active={bool}Reactive class toggleeffect(() => el.classList.toggle("active", bool()))
style:color={value}Reactive inline styleeffect(() => el.style.color = value())
prop:foo={val}Force property assignmentel.foo = val (skips setAttribute)
ref={fn}Ref callbackfn(el) after element is created
const active = signal(false);
const color = signal("blue");
<button
class:active={active}
style:color={color}
on:click={() => active(!active())}
ref={(el) => console.log("Button element:", el)}
>
Toggle
</button>

Attributes from JSX

When the JSX runtime encounters a prop on a custom element it resolves it in this order:

value is reactive (Signal / Computed / () => T)?
yes β†’ effect(() => resolve(el, key, value())) // live, re-runs on change
no β†’ resolve(el, key, value) // set once
resolve(el, key, value):
key in el (property exists β€” e.g. @reactive getter/setter)?
yes β†’ el[key] = value // property assignment
no β†’ el.setAttribute(key, String(value)) // HTML attribute

In practice, given @reactive() count = 0 on the element:

// 1. Reactive β€” effect(() => el.count = countSignal())
// @reactive setter fires on every change β†’ DOM updates
<x-counter count={countSignal} />
// 2. Static + property exists (count is @reactive) β€” el.count = 5
// Property assignment, NOT setAttribute
<x-counter count={5} />
// 3. Static + no property on element β€” setAttribute("data-id", "abc")
// Falls back to setAttribute because "data-id" is not in el
<x-counter data-id="abc" />

If the element has no count property at all (not @reactive, not defined), JSX would call setAttribute("count", "5") β€” which triggers attributeChangedCallback if count is listed in observedAttributes.

This means you can expose an attribute-only API (no JS property) and still receive values from JSX:

@attributes
class BadgeElement extends HTMLElement {
static [attr] = {
// No @reactive property β€” value arrives only via setAttribute / HTML
label(this: BadgeElement, value: string | null) {
this.querySelector("span")!.textContent = value ?? "";
},
};
// No "label" property defined β†’ JSX always uses setAttribute
}
<x-badge label="New" /> // setAttribute("label", "New")
<x-badge label={title} /> // effect(() => el.setAttribute("label", title()))

See also