xhtm-dom
A tiny, framework-free DOM helper for writing HTML-like templates in plain JavaScript or TypeScript.
xhtm-dom combines the xhtm tagged-template parser with a native DOM adapter. It creates real browser Element, SVGElement, DocumentFragment, and Text nodes without a virtual DOM, JSX compiler, or rendering runtime.
Features
- HTML-like tagged templates with
html. - Native DOM node creation through the internal
h helper. - Interpolated text, attributes, properties, children, arrays, and DOM nodes.
- Event listeners such as
onClick and onInput. - Direct DOM property bindings with a leading dot, for example
.value. - HTML and SVG element creation, including mixed
foreignObject content. - Function components with props and
children. - Refs as callback functions or
{ current } objects. - Object and string styles.
data-* and aria-* attributes, plus a dataset object helper.- Fragments for returning multiple top-level nodes.
- TypeScript types for DOM props, events, refs, styles, children, and components.
- ES module, CommonJS, and UMD library builds.
Installation
npm install xhtm-dom
The package exposes compiled files from dist:
import: ES modulerequire: CommonJSdefault/browser: UMD buildtypes: generated TypeScript declarations
The library expects a browser-like DOM. It is suitable for browser scripts, userscripts, and applications that already have document available.
Quick start
import html from "xhtm-dom";
const name = "Ada";
function onClick() {
alert(`Hello, ${name}!`)
}
const button = html`
<button class="greeting" onClick=${onClick}>
Hello, ${name}
</button>
`;
document.body.append(button);
html returns a Node, so its result can be passed directly to append, appendChild, or other DOM APIs.
const content = html`
<h1>Dashboard</h1>
<p>Ready.</p>
`;
document.querySelector("main")?.append(content);
When a template contains multiple top-level nodes, the result is a DocumentFragment. A scalar result is normalized to a Text node.
The html function
html is a tagged-template function:
html`<div>Hello</div>`
Its signature is:
function html(
statics: TemplateStringsArray,
...args: unknown[]
): Node;
Use it by placing JavaScript expressions inside ${...}. The template is parsed once by xhtm, then dynamic values are supplied to the DOM adapter.
Text interpolation
Strings and numbers can be inserted as text. null, undefined, and booleans are ignored when used as children.
const user = "Ada";
const count = 3;
const view = html`
<p>${user} has ${count} messages.</p>
`;
Interpolated values are handled as DOM values rather than being treated as a second HTML source. Build structure with template markup or DOM nodes instead of concatenating untrusted strings into markup.
Dynamic attributes
Expressions can provide attribute values:
const link = "https://example.com";
const label = "Open example";
const view = html`<a href=${link} aria-label=${label}>${label}</a>`;
Boolean values are useful for HTML boolean attributes:
const isDisabled = true;
const view = html`<button disabled=${isDisabled}>Save</button>`;
className and htmlFor are accepted by the DOM adapter and become the HTML attributes class and for when used through h or through template props.
Dynamic children and arrays
DOM nodes, nested arrays, strings, numbers, and components can be used as children. Arrays are flattened recursively.
const items = ["One", "Two", "Three"];
const list = html`
<ul>
${items.map((item) => html`<li>${item}</li>`)}
</ul>
`;
The result of each nested html call is a node and can be inserted into another template. A DocumentFragment is consumed when it is appended, following normal DOM behavior.
Self-closing and optional-close tags
XHTM accepts useful HTML shorthand:
const controls = html`
<input type="search" />
<br />
`;
const paragraphs = html`<p>First<p>Second`;
It also supports normal HTML directives such as <!doctype html> where the target DOM accepts them.
Dynamic tag names
Tag names can be calculated when the template is parsed:
const tag = "section";
const view = html`<${tag} class="panel">Content</${tag}>`;
For reusable logic that needs typed props, a function component is usually clearer than a dynamic tag name.
DOM properties, events, and refs
The adapter uses attributes by default. Prefix a prop with . to assign the corresponding DOM property directly:
const input = html`
<input value="initial" .value=${"live value"} .disabled=${false} />
`;
This distinction matters for live form state: value="..." sets an attribute, while .value=${...} sets the current HTMLInputElement.value property.
Event props beginning with on are registered with addEventListener. Common handlers are typed, including onClick, onInput, onChange, and onKeydown:
const view = html`
<button onClick=${(event: MouseEvent) => {
console.log(event.currentTarget);
}}>
Click
</button>
`;
A ref can be a callback or a mutable object:
const inputRef = { current: null as HTMLInputElement | null };
const view = html`
<input ref=${inputRef} />
`;
inputRef.current?.focus();
Refs are assigned while the element is created. This library does not manage ref cleanup or component lifecycles.
Styles, data, and ARIA attributes
Use a CSS string or an object for style:
const view = html`
<div style=${ { color: "rebeccapurple", padding: "1rem" } }>
Styled content
</div>
`;
The dataset helper converts camelCase keys to data-* attributes:
const view = html`
<div dataset=${ { userId: 42, source: "inbox" } }>Message</div>
`;
data-* and aria-* attributes can also be written directly in markup:
const view = html`
<button data-action="save" aria-label="Save document">Save</button>
`;
Components
A component is a function that receives props and optional children, then returns a DOM node, fragment, text, or an array of supported children.
const Card = (props: { title: string; children?: unknown }) =>
html`<article class="card">
<h2>${props.title}</h2>
${props.children}
</article>`;
The xhtm-dom package's public entry point exports html as the default export. The h helper and supporting types are currently implemented in src/htm.ts for this repository's source and tests; use the published entry point for the supported package API.
The h DOM adapter
The lower-level helper has the shape:
h(tag, props?, ...children)
Examples:
const button = h("button", {
class: "primary",
disabled: false,
onClick: () => console.log("saved"),
}, "Save");
const fragment = h(null, null,
h("span", null, "First"),
h("span", null, "Second"),
);
Behavior includes:
- A string tag creates an HTML element, or a recognized SVG element in the SVG namespace.
- A function tag calls the function with copied props and normalized
children. - A missing tag (
null or undefined) creates a DocumentFragment. Node children are appended directly, including nodes from another DOM realm.- Strings and numbers become text nodes.
null, undefined, and booleans are skipped as children.children in the props object is ignored when explicit children are supplied to h.
What this library does not do
xhtm-dom is a DOM construction utility, not a full UI framework:
- It does not diff or update an existing tree.
- It does not provide state management, effects, routing, or lifecycle hooks.
- Calling
html creates new DOM nodes; it does not automatically reconcile prior output. - Event listeners are attached directly to the created elements.
- Server-side rendering requires a DOM implementation such as
jsdom and an appropriate environment setup.
For updates, keep references to created nodes or replace/modify them with standard DOM APIs.
Userscript build
The Vite userscript entry point assigns the function to globalThis.html:
const view = html`<div class="notice">Loaded</div>`;
document.body.append(view);
The generated userscript bundles xhtm and can be used in a browser userscript manager. The current Vite match configuration targets all HTTP and HTTPS pages; adjust the userscript metadata before publishing a narrower script.
Development
npm install
npm test
npm run build
Additional package commands:
npm run build:types
npm run build:lib
npm run build:package
npm run dev
npm run build:docs
npm run build:docs reads this README and writes a filled GitHub copy to README.md plus a fully rendered HTML copy to docs/README.greasyfork.md. It expands package metadata, converts Markdown to HTML, and renders fenced code blocks with Shiki. Run it again whenever the source README changes before publishing.
This source file uses Handlebars' Mustache-style templates. Package metadata is available under package, so xhtm-dom and 0.0.0 are replaced from package.json during the build.
License
This project is licensed under the MIT License. The userscript build includes xhtm, which is also distributed under the MIT License.