Skip to content

Instantly share code, notes, and snippets.

@thehappycheese
Last active May 21, 2026 16:30
Show Gist options
  • Select an option

  • Save thehappycheese/9fe2da66bfab164779ca8a0f797cd198 to your computer and use it in GitHub Desktop.

Select an option

Save thehappycheese/9fe2da66bfab164779ca8a0f797cd198 to your computer and use it in GitHub Desktop.
hyperscript

A quick and dirty utility to mint DOM elements and avoid using react as long as possible for prototypes :)

h("div", {style:{width:"10em", height:"10em", backgroundColor:"red"}},[
  h("div", {},"hey"),
  h("div", {},"there"),
])
/**
* @template {keyof HTMLElementTagNameMap} K
* @typedef {Object} HOptions
* @property {Partial<Omit<HTMLElementTagNameMap[K], "style" | "dataset" | "classList">>} [props]
* Direct properties on the element: textContent, value, checked, href, disabled, etc.
* @property {Partial<CSSStyleDeclaration>} [style]
* Inline styles.
* @property {Record<string, string>} [attrs]
* Attributes that aren't properties: aria-*, role, custom attrs.
* @property {Record<string, string>} [data]
* Dataset entries (becomes data-* attributes).
* @property {string | string[]} [class]
* Class name(s). String or array, joined with spaces.
* @property {{ [E in keyof GlobalEventHandlersEventMap]?: (this: HTMLElementTagNameMap[K], ev: GlobalEventHandlersEventMap[E]) => void }} [on]
* Event listeners, keyed by event name without the "on" prefix: { click, input, keydown }.
*/
/**
* @typedef {Node | string} HChild
*/
/**
* @template {keyof HTMLElementTagNameMap} K
* @param {K} tag
* @param {HOptions<K>} [options]
* @param {HChild | HChild[]} [children]
* @returns {HTMLElementTagNameMap[K]}
*/
export function h(tag, options = {}, children) {
const el = document.createElement(tag);
const { props, style, attrs, data, class: klass, on } = options;
if (props) Object.assign(el, props);
if (style) Object.assign(el.style, style);
if (attrs) for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
if (data) Object.assign(el.dataset, data);
if (klass) el.className = Array.isArray(klass) ? klass.join(" ") : klass;
if (on) {
for (const [event, handler] of Object.entries(on)) {
el.addEventListener(event, handler);
}
}
if (children != null) {
el.append(...(Array.isArray(children) ? children : [children]));
}
return el;
}
export type HOptions<K extends keyof HTMLElementTagNameMap> = {
/** Direct properties on the element: textContent, value, checked, href, disabled, etc.*/
props?: Partial<Omit<HTMLElementTagNameMap[K], "style" | "dataset" | "classList">>;
/** Inline styles. */
style?: Partial<CSSStyleDeclaration>;
/** Attributes that aren't properties: aria-*, role, custom attrs.*/
attrs?: Record<string, string>;
/** Dataset entries (becomes data-* attributes). */
data?: Record<string, string>;
/** Class name(s). String or array, joined with spaces. */
class?: string | string[];
/** Event listeners, keyed by event name without the "on" prefix: { click, input, keydown }. */
on?: { [E in keyof GlobalEventHandlersEventMap]?: (this: HTMLElementTagNameMap[K], ev: GlobalEventHandlersEventMap[E]) => void };
}
export type HChild = Node | string;
export function h<K extends keyof HTMLElementTagNameMap>(tag:K, options:HOptions<K> = {}, children?:HChild|HChild[]) {
const el = document.createElement(tag);
const { props, style, attrs, data, class: klass, on } = options;
if (props) Object.assign(el, props);
if (style) Object.assign(el.style, style);
if (attrs) for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
if (data) Object.assign(el.dataset, data);
if (klass) el.className = Array.isArray(klass) ? klass.join(" ") : klass;
if (on) {
for (const [event, handler] of Object.entries(on)) {
el.addEventListener(event, handler as any);
}
}
if (children != null) {
el.append(...(Array.isArray(children) ? children : [children]));
}
return el;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment