HTM with VanJS for JSX-like syntax in vanilla JavaScript using VanJS reactivity.
npm install vanjs-htmA flexible and lightweight (<900B gzipped minified) HTM integration for VanJS and optionally VanX, supporting control flow directives, automatic SVG namespace handling, and optional HTML entity decoding.
Here's a sample based on the simplified TODO App from VanJS.
- Tagged Template HTML: Write JSX-like templates in plain JavaScript using HTM with VanJS, no build step required.
- Function Components: Create reusable component functions that work seamlessly with all VanHTM features including control flow directives.
- Control Flow Directives: Use for:each, show:when, and portal:mount for SolidJS style declarative rendering. You can also combine show:when with for:each and portal:mount to conditionally render lists and portals. Note: VanX is required only for the for:each directive.
- Automatic SVG Support: SVG elements are automatically rendered with the correct namespace. Use the vh:svg directive for excluded or ambiguous elements.
- Optional HTML Entity Decoding: Decode HTML entities in string children (requires a HTML entities library like entities, he, html-entities, etc.).
- TypeScript Support: VanHTM is written in TypeScript and provides full type definitions.
``js
// Script tags for including van and vanX
//
//
// Script tags for including htm and vanHTM
//
//
// The imports below can be replaced by the script tags above for htm and vanHTM
import htm from 'htm';
import vanHTM from 'vanjs-htm';
// const { html, rmPortals } = vanHTM({ htm, van, vanX }); // This line and the one below are interchangeable
const { html, rmPortals } = vanHTM({ htm, van, vanX: { list: vanX.list } });
const el = html
;
van.add(document.body, el);
`Local Sandbox
The repository includes a sandbox environment for experimenting with VanHTM locally. To run it:
`bash
npm install
npm run sandbox
`This will start a local development server where you can explore and test VanHTM features.
Browser Builds
VanHTM provides several prebuilt bundles for browser usage, available via CDN (e.g., jsDelivr). You can choose the build that best fits your needs.
Build output structure:
-
dist/ default builds.
- dist/withDecoding/ builds that utilize HTML Entity Decoding (requires a HTML entities library like entities, he, html-entities, etc.).Each directory contains:
-
van-htm.module.js (ESM, minified, ~870B gzipped)
- van-htm.js (IIFE/global, minified, ~880B gzipped)
- van-htm.cjs (CJS, minified)
- van-htm.dev.module.js (ESM, unminified)
- van-htm.dev.js (IIFE/global, unminified)Function Components
VanHTM supports function components, allowing you to create reusable UI components that work seamlessly with all VanHTM features including control flow directives.
$3
`js
// Define a reusable component
const Card = (props, ...children) => html;const el = html
This is the card content
;van.add(document.body, el);
`Control Flow Directives
$3
Renders a list by looping over a reactive array or iterable. The value of
for:each should be a reactive list (e.g., from vanX.reactive). The child function receives the current value, a deleter function, and the index/key.Note: This directive requires VanX. If
vanX is not provided to vanHTM() and you attempt to use for:each, an error will occur.`js
const items = vanX.reactive([1, 2, 3]);
van.add(
document.body,
html
- ${v}
}
);
`See VanX docs: Reactive List for more details on the
itemFunc parameter.$3
Conditionally renders content based on a boolean, a VanJS state, or a function. If the condition is falsy, the
show:fallback value is rendered instead (can be a primitive, a state or a function if you need reactivity).Note: Due to how HTM works, children are evaluated (eagerly) before the
show:when condition is checked. For complex children or performance-sensitive code, consider using a function to defer evaluation:`js
// ❌ Expensive operation runs even when condition is false due to
html// ✅ Use a function for complex/expensive children
html
html...complex children...}// ✅ Or use a conditional function directly
${() => condition ? html
: ''}
${() => condition ? html : ''}
``js
const visible = van.state(true);
const toggleButton = html;
van.add(
document.body,
html
Fallback - ${visible}
}
);
`-
show:when: Accepts a boolean, a VanJS state, or a function returning a boolean.
- show:fallback: (Optional) Content to render when the condition is falsy. Can be a primitive, a state or a function if you need reactivity.$3
Renders the element into a different part of the DOM (a "portal"). The
portal:mount attribute determines where the content is rendered. It can be:- A DOM
Node
- A CSS selector string (e.g., #modal-root)> Note: For
rmPortals to work correctly, portals should only be the direct child of their parent element. Nesting portals deeper will prevent rmPortals from removing them properly.> Implementation Detail: VanHTM automatically adds a
p:id attribute to portaled elements for internal tracking. This attribute is used by rmPortals to identify and remove the correct portal elements. You should not manually set or modify this attribute. See below for more information.`js
const portalTarget = document.getElementById('portal-target');
const containerWithPortal = html;
van.add(document.body, containerWithPortal);
`You can also use a selector:
`js
const portalTargetId = '#portal-target';
const containerWithPortal = html;
van.add(document.body, containerWithPortal);
`$3
`js
// Removes all portaled elements created from parentContainer that are mounted in portalTarget.
// If no portalTarget is specified, it defaults to document.body.
rmPortals(parentContainer, portalTarget?);
`Parameters:
-
parentContainer (Node): The container element that contains the portal placeholder comments
- portalTarget (Element | string, optional): The target where portal content was mounted. Can be:
- A DOM Element
- A CSS selector string (e.g., '#modal-root', '.portal-container')
- If omitted, defaults to document.bodyExamples:
`js
// Remove portals mounted in a specific element
rmPortals(containerWithPortal, document.getElementById('modal-root'));// Remove portals mounted using a CSS selector
rmPortals(containerWithPortal, '#modal-root');
// Remove portals mounted in document.body (default behavior)
rmPortals(containerWithPortal);
// Equivalent to:
rmPortals(containerWithPortal, document.body);
`$3
You can combine the
show:when directive with for:each and portal:mount on the same element to conditionally render lists or portaled elements. If the show:when condition is falsy, neither the list nor the portal will be rendered, and the show:fallback (if provided) will be used instead.Example: Conditionally render a list
`js
const items = vanX.reactive([1, 2, 3]);
const showList = van.state(true);van.add(
document.body,
html
- ${v}
}
);
`Example: Conditionally render a portal
`js
const getTime = () => new Date().toLocaleTimeString();
const portalTarget = document.getElementById('portal-target');
const showPortal = van.state(true);
const time = van.state(getTime());const intervalId = setInterval(() => {
time.val = getTime();
}, 1000);
const container = html
;
van.add(document.getElementById('main-content'), container);
`SVG Support
VanHTM automatically handles SVG elements by applying the correct namespace when rendering. This ensures that SVG elements work properly without any additional configuration.
$3
The following SVG elements are automatically rendered with the SVG namespace:
Shapes:
circle, ellipse, line, path, polygon, polyline, rect
Container elements: svg, g, defs, symbol, use
Gradient and pattern elements: linearGradient, radialGradient, stop, pattern
Text elements: text, textPath, tspan
Other common elements: clipPath, desc, filter, foreignObject, marker, mask`js
// Basic SVG with automatic namespace handling
const radius = van.state(30);
const basicSVG = html;
van.add(document.body, basicSVG);// Complex SVG with gradients and paths
const complexSVG = html
;
van.add(document.body, complexSVG);
`$3
To keep the bundle size small, some SVG elements are excluded from automatic namespace handling:
- Animation elements:
animate, animateMotion, animateTransform, set
- Filter effect elements: All fe* elements (e.g., feGaussianBlur, feBlend, feColorMatrix, etc.)
- Other elements: metadata, mpath, switch, view$3
For excluded elements or when you need explicit control, use the
vh:svg directive to force SVG namespace:`js
// Animated SVG using vh:svg directive for excluded elements
const animatedCircle = html;
van.add(document.body, animatedCircle);// SVG with filter effects using vh:svg
const blurredRect = html
;
van.add(document.body, blurredRect);
`$3
Some elements exist in both HTML and SVG (
a, script, style, title). These default to HTML namespace for compatibility:`js
// Using both HTML and SVG styles
const styledSVG = html;
van.add(document.body, styledSVG);
`Optional HTML Entity Decoding
`js
import { decode } from 'html-entities';
import vanHTM from 'vanjs-htm/withDecoding';// const { html, rmPortals } = vanHTM({ htm, van, vanX, decode }); // This line and the one below are interchangeable
const { html, rmPortals } = vanHTM({ htm, van, vanX: { list: vanX.list }, decode });
// Example below
const el = html
;
van.add(document.body, el);
`API
$3
-
htm: Required in all builds. The HTM instance.
- van: Required in all builds. The VanJS instance.
- vanX: Required only for the for:each directive. The VanJS Extension instance or an object that contains a list property set as vanX.list. If not provided and for:each is used, an error will occur.
- decode: Required in builds that include HTML Entity Decoding (vanjs-htm/withDecoding). The decode method from a HTML entities library like entities, he, html-entities, etc.Returns:
-
html: The htm template tag.
- rmPortals(parentContainer: Node, portalTarget?: Element | string): Remove portaled elements created from parentContainer. The portalTarget parameter specifies where to look for the portal content:
- Can be an Element or a CSS selector string
- Defaults to document.body if not provided
- Refer to the examples here.Technical Details
$3
- Invalid for:each Data: The
for:each directive relies on VanX's list function. Refer to VanX documentation for error handling behavior with invalid reactive data.
- Invalid Portal Selectors: If a CSS selector provided to portal:mount doesn't match any element, VanJS will throw an error when attempting to mount the portal content.
- Missing Portal Targets: If rmPortals is called with an invalid selector or non-existent element, the function will silently return without performing any operations.
- Missing VanX for for:each: If vanX is not provided to vanHTM() and the for:each directive is used, an error will occur.$3
VanHTM explicitly disables HTM's template string caching mechanism by setting
this[0] = 3 in the template processor. This ensures that each template evaluation creates fresh elements, which is necessary for proper VanJS reactivity and state management. Refer to HTM documentation on Caching for more information.$3
VanHTM automatically adds a
p:id attribute to portaled elements for internal tracking. This attribute uses an auto-incrementing counter (format: p-${counter}) and is used by rmPortals` to identify and remove the correct portal elements. You should not manually set or modify this attribute.MIT