npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

vanjs-htm

v1.2.0

Published

HTM with VanJS for JSX-like syntax in vanilla JavaScript using VanJS reactivity.

Readme

VanHTM

A 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.

Features

Usage

Try on CodePen

// Script tags for including van and vanX
// <script src="https://cdn.jsdelivr.net/gh/vanjs-org/van/public/van-latest.nomodule.min.js"></script>
// <script src="https://cdn.jsdelivr.net/npm/vanjs-ext/dist/van-x.nomodule.min.js"></script>

// Script tags for including htm and vanHTM
// <script src="https://cdn.jsdelivr.net/npm/htm/dist/htm.js"></script>
// <script src="https://cdn.jsdelivr.net/npm/vanjs-htm/dist/van-htm.js"></script>
// 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`
  <div>
    Hello,
    <b>world</b>
    !
  </div>
`;
van.add(document.body, el);

Local Sandbox

The repository includes a sandbox environment for experimenting with VanHTM locally. To run it:

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:

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.

Basic Function Component

Try on CodePen

// Define a reusable component
const Card = (props, ...children) => html`
  <div class="card">
    <h3>${props.title}</h3>
    <div class="card-content">${children.length ? children : 'No children provided'}</div>
  </div>
`;

const el = html`
  <${Card} title="Welcome">
    <p>This is the card content</p>
    <button>Click me</button>
  <//>
`;

van.add(document.body, el);

Control Flow Directives

for:each

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.

Try on CodePen

const items = vanX.reactive([1, 2, 3]);
van.add(
  document.body,
  html`
    <ul for:each=${items}>
      ${(v, deleter, k) =>
        html`
          <li>${v}</li>
        `}
    </ul>
  `
);

See VanX docs: Reactive List for more details on the itemFunc parameter.

show:when

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:

// ❌ Expensive operation runs even when condition is false due to
html`<div show:when=${condition}>${expensiveOperation()}</div>`

// ✅ Use a function for complex/expensive children
html`<div show:when=${condition}>${expensiveOperation}</div>`
html`<div show:when=${condition}>${() => html`...complex children...`}</div>`

// ✅ Or use a conditional function directly
${() => condition ? html`<div>${expensiveOperation()}</div>` : ''}
${() => condition ? html`<div>...complex children...</div>` : ''}

Try on CodePen

const visible = van.state(true);
const toggleButton = html`
  <button onclick=${() => (visible.val = !visible.val)}>Toggle Visible</button>
`;
van.add(
  document.body,
  html`
    <div>
      ${toggleButton}
      <div
        show:when=${visible}
        show:fallback=${() =>
          html`
            <div><b>Fallback - ${visible}</b></div>
          `}
      >
        Visible - ${visible}
      </div>
    </div>
  `
);
  • 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.

portal:mount

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.

Try on CodePen

const portalTarget = document.getElementById('portal-target');
const containerWithPortal = html`
  <div>
    <div>Some content before</div>
    <div portal:mount=${portalTarget}>Content to Portal</div>
    <div>Some content after</div>
    <button onclick=${() => rmPortals(containerWithPortal, portalTarget)}>Remove Portal</button>
  </div>
`;
van.add(document.body, containerWithPortal);

You can also use a selector:

Try on CodePen

const portalTargetId = '#portal-target';
const containerWithPortal = html`
  <div>
    <div>Some content before</div>
    <div portal:mount=${portalTargetId}>Content to Portal</div>
    <div>Some content after</div>
    <button onclick=${() => rmPortals(containerWithPortal, portalTargetId)}>Remove Portal</button>
  </div>
`;
van.add(document.body, containerWithPortal);

Removing Portaled Elements

// 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.body

Examples:

// 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);

Combining show:when with for:each and portal:mount

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

Try on CodePen

const items = vanX.reactive([1, 2, 3]);
const showList = van.state(true);

van.add(
  document.body,
  html`
    <button onclick=${() => (showList.val = !showList.val)}>Toggle List</button>
    <button onclick=${() => items.push(Object.keys(items).length + 1)}>Add item</button>
    <ul for:each=${items} show:when=${showList}>
      ${(v) =>
        html`
          <li>${v}</li>
        `}
    </ul>
  `
);

Example: Conditionally render a portal

Try on CodePen

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`
  <div>
    <div portal:mount=${portalTarget} show:when=${showPortal}>Portaled Content ${time}</div>
    <button onclick=${() => (showPortal.val = !showPortal.val)}>Toggle Portal</button>
  </div>
`;
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.

Automatically Handled SVG Elements

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

Try on CodePen

// Basic SVG with automatic namespace handling
const radius = van.state(30);
const basicSVG = html`
  <svg width="100" height="100">
    <circle cx="50" cy="50" r=${radius} fill="lightblue" stroke="darkblue" stroke-width="2" />
    <text x="50" y="55" text-anchor="middle" fill="darkblue">SVG</text>
  </svg>
  <br />
  <input type="range" min="10" max="45" value=${radius} oninput=${(e) => (radius.val = parseInt(e.target.value))} />
  <span>Radius: ${radius}</span>
  <br />
`;
van.add(document.body, basicSVG);

// Complex SVG with gradients and paths
const complexSVG = html`
  <svg width="200" height="100">
    <defs>
      <linearGradient id="gradient">
        <stop offset="0%" stop-color="#f00" />
        <stop offset="100%" stop-color="#00f" />
      </linearGradient>
    </defs>
    <rect x="10" y="10" width="180" height="80" fill="url(#gradient)" rx="10" />
    <path d="M 50 50 L 150 50" stroke="white" stroke-width="3" />
  </svg>
`;
van.add(document.body, complexSVG);

Excluded Elements

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

vh:svg Directive

For excluded elements or when you need explicit control, use the vh:svg directive to force SVG namespace:

Try on CodePen

// Animated SVG using vh:svg directive for excluded elements
const animatedCircle = html`
  <svg width="200" height="200">
    <circle cx="100" cy="100" r="40" fill="purple">
      <!-- animate is excluded, so we need vh:svg -->
      <animate vh:svg attributeName="r" values="40;60;40" dur="2s" repeatCount="indefinite" />
    </circle>
  </svg>
`;
van.add(document.body, animatedCircle);

// SVG with filter effects using vh:svg
const blurredRect = html`
  <svg width="200" height="200">
    <defs>
      <filter id="blur">
        <!-- feGaussianBlur is excluded, so we need vh:svg -->
        <feGaussianBlur vh:svg in="SourceGraphic" stdDeviation="5" />
      </filter>
    </defs>
    <rect x="50" y="50" width="100" height="100" fill="orange" filter="url(#blur)" />
  </svg>
`;
van.add(document.body, blurredRect);

Shared HTML/SVG Elements

Some elements exist in both HTML and SVG (a, script, style, title). These default to HTML namespace for compatibility:

Try on CodePen

// Using both HTML and SVG styles
const styledSVG = html`
  <div>
    <style>
      .highlight {
        fill: yellow;
      }
    </style>
    <svg width="100" height="100">
      <style vh:svg>
        .svgtext {
          font-size: 20px;
        }
      </style>
      <circle cx="50" cy="50" r="40" class="highlight" />
      <text x="50" y="55" text-anchor="middle" class="svgtext">Hi</text>
    </svg>
  </div>
`;
van.add(document.body, styledSVG);

Optional HTML Entity Decoding

Try on CodePen

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`
  <div>
    Hello,
    <b>world</b>
    !&nbsp;&#128526;
  </div>
`;
van.add(document.body, el);

API

vanHTM(options)

  • 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

Error Handling

  • 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.

HTM Caching Behavior

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.

Portal Implementation

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.

License

MIT