Skip to content

Instantly share code, notes, and snippets.

@webstrand
Last active June 4, 2026 06:20
Show Gist options
  • Select an option

  • Save webstrand/fb91dc3c0ad1008bdd9161a191ce019c to your computer and use it in GitHub Desktop.

Select an option

Save webstrand/fb91dc3c0ad1008bdd9161a191ce019c to your computer and use it in GitHub Desktop.
/**
* Creates (or augments) a DOM element, then populates it with children and properties.
*
* `h` is polymorphic in both arguments: the first selects/creates the element,
* and every remaining argument is a "child" interpreted by its runtime type.
*
* @param {string | Node | [namespaceURI: string, qualifiedName: string]} tag
* Selects the element to operate on:
* - **string** ― a tag name passed to `document.createElement` (e.g. `"div"`).
* - **Node** ― an existing element (anything with a numeric `nodeType`). It is
* used as-is; children and props are applied to it rather than to a new element.
* - **array** ― spread into `document.createElementNS`, i.e.
* `[namespaceURI, qualifiedName]`, for namespaced elements such as SVG.
*
* @param {...(string | number | bigint | boolean | symbol | null | undefined | Node | Function | Iterable<*> | Object)} children
* Zero or more children, each interpreted by its runtime type:
* - **string** ― appended as a text node.
* - **number | bigint** ― stringified, then appended as a text node.
* - **boolean | symbol** ― ignored, so `cond && child` appends nothing when `cond` is false.
* - **null | undefined** ― ignored.
* - **function** ― called with the element as its only argument; its return value
* is processed as further children.
* - **Node** ― appended directly.
* - **iterable** (array, Set, generator, ...) ― flattened, each item processed
* recursively. Strings are handled above, not here.
* - **plain object** ― a bag of properties/attributes. Own, enumerable keys
* (a value of `undefined` is skipped) are applied by key:
* - `class` ― assigned to `el.className` on HTML; routed to `setAttribute`
* on SVG, where `className` is a read-only `SVGAnimatedString`.
* - `for` ― assigned to `el.htmlFor`.
* - `classList` ― spread into `el.classList.add(...value)`.
* - `style` / `dataset` ― an object value is merged with `Object.assign`;
* a string value falls back to `setAttribute`.
* - `attributes` ― a nested object; every entry is set as an attribute
* (see "attribute values" below), whatever its key.
* - a key containing a colon ― always set as an attribute, never a property.
* - any other key ― assigned as a property when the element has a *writable*
* slot for it; otherwise set as an attribute. (SVG's reflected attributes
* such as `cx`/`width` surface as read-only `SVGAnimated*` objects, so they
* fall through to `setAttribute` rather than throwing.)
*
* Attribute values (in the `attributes` block, on colon keys, and on
* non-property keys) all go through one code path and are interpreted by type:
* `undefined` is skipped, `boolean` toggles via `toggleAttribute`, `null`
* removes via `removeAttribute`, a `[namespaceURI, value]` array *on a name
* containing a colon* is set with `setAttributeNS` (an array on a colon-less
* name is just stringified), and anything else is stringified and set with
* `setAttribute`. Within a namespaced array, a boolean `value` toggles too:
* `true` sets the empty string, `false` removes it via `removeAttributeNS`
* (which takes the local name only).
*
* @returns {Node} The created (or passed-in) element, after children and props are applied.
*
* @example
* // Nested elements, text, props, and an event handler
* h("button",
* { class: "primary", onclick: () => alert("hi") },
* "Click ", h("strong", "me"));
*
* @example
* // Namespaced (SVG) element and a namespaced attribute
* const SVG = "http://www.w3.org/2000/svg";
* const XLINK = "http://www.w3.org/1999/xlink";
* h([SVG, "svg"], { viewBox: "0 0 10 10" },
* h([SVG, "use"], { "xlink:href": [XLINK, "#icon"] }));
*
* @example
* // Iterables are flattened; null/false children are skipped
* h("ul", items.map(t => h("li", t)), showFoot && h("li", "footer"));
*/
function h(tag, ...children) {
const el = typeof tag.nodeType === "number"
? tag
: Array.isArray(tag)
? document.createElementNS(...tag)
: document.createElement(tag);
annotate(el, ...children);
return el;
}
/** Appends children to `el`, interpreting each by runtime type (internal; see `h`). */
function annotate(el, ...children) { for (let c of children) switch (typeof c) {
case "boolean":
case "symbol":
break;
default:
c = String(c);
case "string":
el.append(c);
break;
case "function":
annotate(el, c(el));
break;
case "undefined":
case "object":
if (c == null) break;
else if (typeof c.nodeType === "number") {
el.append(c);
} else if (typeof c[Symbol.iterator] === "function") {
for (const item of c) annotate(el, item);
} else {
for (const k in c) {
if (!Object.prototype.hasOwnProperty.call(c, k)) continue;
const v = c[k];
if (v === undefined) continue;
switch (k) {
case "class":
if (assignable(el, "className")) el.className = v;
else el.setAttribute("class", v);
break;
case "for": el.htmlFor = v; break;
case "classList":
el.classList.add(...v);
break;
case "style":
case "dataset":
if (typeof v === "object") Object.assign(el[k], v);
else el.setAttribute(k, v); // harmless dataset=string
break;
case "attributes":
for (const n in v) {
if (!Object.prototype.hasOwnProperty.call(v, n)) continue;
annotateAttr(el, n, v[n]);
}
break;
default: {
const colon = k.indexOf(":");
if (colon === -1 && assignable(el, k)) el[k] = v;
else annotateAttr(el, k, v, colon);
}
}
}
}
break;
} }
/**
* Sets one attribute on `el`, dispatched by the value's runtime type (internal; see `h`).
* `colon`, when provided, is `name.indexOf(":")`; otherwise it is derived lazily and
* only when needed (i.e. when the value is a `[namespaceURI, value]` array).
*/
function annotateAttr(el, name, v, colon) {
switch (typeof v) {
case "undefined": break;
case "boolean":
el.toggleAttribute(name, v);
break;
default:
if (v === null) {
el.removeAttribute(name);
break;
}
if (Array.isArray(v) && (colon ??= name.indexOf(":")) !== -1) {
const [ns, val] = v;
if (typeof val === "boolean") {
if (val) el.setAttributeNS(ns, name, "");
else el.removeAttributeNS(ns, name.slice(colon + 1));
} else {
el.setAttributeNS(ns, name, String(val));
}
break;
}
v = String(v);
case "string":
el.setAttribute(name, v);
break;
}
}
/**
* True when `el[name] = value` is a usable write ― the slot exists and is not a
* read-only DOM wrapper (the `SVGAnimated*` objects, `SVGPointList`, etc., which
* surface as non-null objects). `null` counts as writable, so event-handler slots
* like `onclick` (default `null`) still bind as properties rather than stringifying.
*
* Caveat: a custom element with a writable property defaulting to a non-null object,
* or re-annotating an element whose object-valued property is already set, will be
* routed to `setAttribute` instead.
*/
function assignable(el, name) {
const cur = el[name];
return name in el && cur !== undefined && (cur === null || typeof cur !== "object");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment