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:
constel= (
<buttonclass="primary"on:click={() =>submit()}>
Save
</button>
);
// Compiles to exactly this:
constel= 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.
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
constcount=signal(0);
count(); // β 0 (read)
count(1); // write β effects depending on count will re-run
count(); // β 1
signal,
functioneffect(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
consturl=signal('/api/data');
conststop=effect(() => {
constcontroller=newAbortController();
fetch(url(), { signal: controller.signal });
onCleanup(() => controller.abort());
});
url('/api/other'); // previous fetch is aborted, new one starts
In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.
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
consturl=signal('/api/data');
conststop=effect(() => {
constcontroller=newAbortController();
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(() => {
constel:JSX$1.Element
el.
any
disabled=
constdisabled: () =>boolean (+1overload)
disabled(); });
functioneffect(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
consturl=signal('/api/data');
conststop=effect(() => {
constcontroller=newAbortController();
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.
// 3. Static + no property on element β setAttribute("data-id", "abc")
// Falls back to setAttribute because "data-id" is not in el
<x-counterdata-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
classBadgeElementextendsHTMLElement {
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