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.
When a style property changes reactively, style:foo={signal} writes only that property. The bulk form style={{ color: c(), background: b() }} re-runs Object.assign on every effect tick, rewriting every key β even unchanged ones. For a single static object, style={obj} is fine; for any value driven by a signal, reach for style:foo.
// Slow on tick β Object.assign rewrites both keys every time c() changes
// Fast β only the color property is written when c() changes
<divstyle:color={c}style:background="white" />
Large lists: consider event delegation
on:click={fn} calls addEventListener on the element. For lists or tables with hundreds of rows where every row carries the same handler, thatβs N listeners β measurable mount cost. For those cases, attach a single listener to the parent and look up the row from a data source via data-id. See the event delegation recipe.
Attributes from JSX
When the JSX runtime encounters a prop on a custom element it resolves it in this order:
// 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