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
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: MaybeReactiveProps<WithJsxNamespaces<Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, UnsupportedDomKeys>, HTMLButtonElement>> & {
ref?: ((el: HTMLButtonElement) => void) | undefined;
children?: Children;
}
button
disabled?: MaybeReactive<boolean> | undefined
disabled
={
const disabled: Updater<boolean> & Computed<boolean>
disabled
}>{
const label: Updater<string> & Computed<string>
label
}</
button: MaybeReactiveProps<WithJsxNamespaces<Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, UnsupportedDomKeys>, HTMLButtonElement>> & {
ref?: ((el: HTMLButtonElement) => void) | undefined;
children?: Children;
}
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
effect
(() => {
const el: JSX$1.Element
el
.
any
disabled
=
const disabled: () => boolean (+1 overload)
disabled
(); });
function effect(fn: () => void): () => void
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>

Reactive styles: prefer style:foo

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
<div style={{ color: c(), background: "white" }} />
// Fast β€” only the color property is written when c() changes
<div style: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:

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