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.
In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.
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