Element Functions
Every HTML element is a typed function.
Signature
Section titled “Signature”function div(props?: CommonProps | Child, ...children: Child[]): WhisqNodefunction span(props?: CommonProps | Child, ...children: Child[]): WhisqNodefunction button(props?: ButtonProps | Child, ...children: Child[]): WhisqNodefunction input(props?: InputProps): WhisqNode// ... and 30+ moreTwo Call Signatures
Section titled “Two Call Signatures”// With props + childrendiv({ class: "card", id: "main" }, h1("Title"), p("Content"))
// Without props — just childrendiv(h1("Title"), p("Content"))
// Single text childh1("Hello Whisq")Available Elements
Section titled “Available Elements”Layout: div, span, main, section, article, aside, header, footer, nav
Text: h1–h6, p, strong, em, small, pre, code
Interactive: button, a
Forms: form, input, textarea, select, option, label
Lists: ul, ol, li
Table: table, thead, tbody, tr, th, td
Media: img, video, audio
Misc: br, hr, iframe
Reactive Props
Section titled “Reactive Props”Pass a function for any prop to make it reactive:
div({ class: () => active.value ? "on" : "off" })div({ style: () => `color: ${color.value}` })div({ hidden: () => !visible.value })class: array form (since alpha.8)
Section titled “class: array form (since alpha.8)”class: also accepts an array of sources. Strings are class names; falsy values (false | null | undefined | 0 | "") are filtered out; functions are reactive (re-run when tracked signals change). If any element is a function, the whole array is applied reactively; otherwise it’s applied once at mount.
div({ class: [ "btn", // static () => `btn-${variant.value}`, // reactive loading.value && "btn-loading", // static conditional shorthand () => isDisabled.value && "disabled", // reactive conditional ],});Eliminates the cx vs rcx decision for the common case — reach for those only when composing class strings outside an element prop.
The runtime walks the array once and joins the kept sources with a single space. Exact behaviour:
-
Skipped (filtered out):
false,null,undefined,0,"". This is what makes thecond && "active"shorthand work — a falsecondshort-circuits to the falsy boolean and is dropped. -
Strings are kept as-is. They are not parsed, trimmed, or split on spaces —
"btn primary"is added verbatim, so you can still pack multiple class names into a single string if you want. -
Truthy non-strings (not functions) are silently dropped. Numbers other than
0, plain objects, arrays, andtrueare not coerced viaString()— they fall through the loop and are not added. The TypeScript type (ClassArraySource) refuses them at the boundary; this rule is the runtime contract for cases that slip through (e.g.any, untyped data). -
Nested arrays are NOT flattened. A nested array is a non-string non-function value, so it’s dropped rather than recursed into:
// ❌ "btn" only — the inner array is droppeddiv({ class: ["btn", ["primary", "lg"]] });// ✅ Spread the inner array, or flatten it yourselfdiv({ class: ["btn", ...["primary", "lg"]] }); -
Functions are called in a reactive getter, and their return value follows the same rules: a truthy string is kept, a falsy value (
false | null | undefined | 0 | "") is skipped. A function that returns an array is not flattened — the array ends up joined via its default.toString()(comma-joined), which is almost certainly a bug. Return a pre-joined string instead:// ❌ Produces `class="btn a,b"` — Array.toString runsdiv({ class: ["btn", () => active.value ? ["a", "b"] : ""] });// ✅ Return a string with spaces — or use separate entriesdiv({ class: ["btn", () => active.value && "a b"] });div({ class: ["btn", () => active.value && "a", () => active.value && "b"] });
ARIA attributes (since alpha.9)
Section titled “ARIA attributes (since alpha.9)”For guide-level treatment (focus management, semantic HTML choices, escape hatch), see the Accessibility guide. The reference below is the prop surface and serialisation rules.
Every element accepts typed aria-* props. Mirrors the data-* pattern but with a wider value type, because ARIA uses both enum-strings (aria-live: "polite") and predicate-booleans (aria-expanded, aria-hidden, aria-pressed). Types are ReactiveProp<string | boolean | undefined> — static values, reactive getters, or undefined to remove the attribute.
// Staticbutton({ "aria-label": "Remove" }, "×");
// Reactivebutton({ "aria-expanded": () => menuOpen.value }, "Menu");div({ "aria-live": "polite", "aria-atomic": true }, () => status.value);
// Conditional removaldiv({ "aria-hidden": () => visible.value ? undefined : true });Serialisation rules:
| Value | Serialised as |
|---|---|
"polite" / string | "polite" |
true | "true" (per ARIA spec — fixed in alpha.9) |
false | "false" |
undefined / null | attribute removed |
Note: pre-alpha.9, true serialised to the empty string aria-expanded="", which is invalid per the ARIA spec (not equivalent to aria-expanded="true"). Alpha.9 routes aria-* through a dedicated branch in the prop applier to produce the correct string forms.
Events
Section titled “Events”button({ onclick: () => count.value++ }, "Click")input({ oninput: (e) => name.value = e.target.value })Docs current to v0.1.0-alpha.9 . All releases →