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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wix/htmdx

v4.11.1

Published

Render editable HTMDX source inside plain HTML artifacts.

Downloads

4,356

Readme

@wix/htmdx

Render editable MDX-like source inside a plain HTML file. @wix/htmdx v4 uses one component definition model for Built-ins, shadcn, and host extensions. HTMDX is built for artifacts that should be easy for people to view and easy for agents to edit.

Live examples: examples index · decision brief · blank canvas · component tour · Storybook — every page is itself an htmdx artifact; view source to see what an agent edits.

Start with one HTML file:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <script src="https://cdn.jsdelivr.net/npm/@wix/[email protected]/dist/browser.js" defer></script>
  </head>
  <body>
    <!-- prettier-ignore -->
    <script
      type="text/htmdx"
      data-htmdx-edit-instruction="Edit only this script content. HTMDX format."
    >
# Title

<ExecutiveSummary>
Agents edit source. Users view rendered HTML.
</ExecutiveSummary>
    </script>
  </body>
</html>

The runtime auto-mounts each bare source block: it wraps the script in a generated <htmdx-code> host in place and renders there. Write <htmdx-code> yourself only when you need explicit output placement or src; disable scanning with register({ automount: false }).

Source block notes:

  • <script type="text/htmdx"> is the canonical source holder. Browsers store its content as raw text, so component tag casing, code fences containing HTML, and angle brackets in prose survive byte-for-byte, and HTML formatters leave the content alone.
  • <template type="text/htmdx"> is also supported, but its content is HTML-parsed and re-serialized, which can rewrite the source (lowercased component tags, restructured code fences).
  • A literal </script> inside the source ends the block early; keep such examples in an external src file instead.

CDN caveats:

  • Generated artifacts can load the browser bundle from jsDelivr after the package is published to npm.
  • Pin an explicit package version in generated artifacts. Do not use floating aliases like @latest, because saved HTML artifacts must keep rendering the same runtime over time.

Exact-version component manifest

Every release includes its machine-readable component contract at:

https://cdn.jsdelivr.net/npm/@wix/[email protected]/dist/components.json

Use the same exact version as the artifact's runtime URL. The htmdx@2 manifest lists the full runtime catalog — Built-ins plus the shadcn/ui pack, each entry tagged with its source, body mode, prop schema, and canonical example. It projects this data from the same definitions used by the runtime; the executable Component field does not appear in JSON.

Each manifest entry declares body: "markdown" | "htmdx" | "none". markdown passes raw Markdown to the component and rejects nested tags; Built-ins use this mode. htmdx accepts Markdown, HTML, and nested registered component tags; shadcn and external definitions use it when they support composition. none accepts only an empty or self-closing tag. A Built-in's purpose and example describe any stricter list or table grammar it checks; npx @wix/htmdx skill components prints the same grammar per family in prose, with a worked example per family. Invalid bodies fail the whole compile, and browser hosts show the error with the raw source. Imports, exports, brace expressions, event handlers, and function-valued props cannot be expressed — the source is data, not code.

Raw HTML

Ordinary HTML renders alongside Markdown, from an allowlist:

Watch <a href="https://wix.com">the announcement</a> or play it here.

<video controls width="640" poster="poster.png">
  <source src="clip.webm" type="video/webm">
</video>

<iframe src="https://example.com/embed" width="560" height="315" allowfullscreen></iframe>

<div class="grid gap-4">

## Still Markdown in here

<Badge>Shipped</Badge>

</div>

Structural, text-level, table, and media elements are allowed — p, div, section, figure, details, table, a, span, br, video, audio, iframe, and friends. Anything outside the list (form, input, object, svg, unknown tags) is not markup at the top level, so it stays literal text the way it always has.

Inside a component body, a tag outside the list still renders the way it did before the allowlist existed — it is passed through with its attributes — so documents written against the old behavior keep compiling. The exception is the handful of elements that turn source into code (script, style, link, meta, base, embed, object, template), which fail the compile.

A registered component still wins on name collision, as before: with the shadcn pack registered, <table> and <button> resolve to Table and Button.

A block element that opens a line owns everything up to its close tag, so blank lines, Markdown, and nested component tags inside it keep working. HTML written mid-sentence renders inline. HTML inside code fences and code spans stays literal, as before.

Only allowlisted attributes survive: global ones (class, id, title, lang, dir, role, style, tabindex, hidden), aria-*, data-*, and a per-element set. href, src, cite, poster, and srcset are scheme checked the same way Markdown links are; style is parsed into a React style object with url() values checked and expression() dropped; on* attributes fail the compile; iframe cannot set srcdoc. The source still cannot express code, only data.

Inline SVG

Vector graphics are written in the source, so a chart, diagram, or icon needs no image file and no component:

<svg viewBox="0 0 320 120" width="320" height="120" role="img">
  <title>Quarterly revenue</title>
  <defs>
    <linearGradient id="bars" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0" stop-color="#6366f1"></stop>
      <stop offset="1" stop-color="#a855f7"></stop>
    </linearGradient>
  </defs>
  <rect x="16" y="70" width="48" height="40" rx="4" fill="url(#bars)"></rect>
  <text x="16" y="12" font-size="11">
    Revenue by quarter
  </text>
</svg>

SVG answers to its own allowlist, separate from the HTML one, because it has its own element and attribute space. Shapes, g, defs, gradients, pattern, clipPath, mask, marker, symbol, switch, text/tspan/textPath, and the filter primitives are allowed. Element names keep their casing, so <linearGradient> and <clipPath> work as written — and <lineargradient> is corrected to match.

A graphic can move on its own. <animateTransform> and <animateMotion> (with <mpath>) are allowed, so a spinner or a marker travelling a path needs no script and no CSS:

<svg viewBox="0 0 40 40" width="40" height="40">
  <rect x="10" y="10" width="20" height="20" fill="#6366f1">
    <animateTransform
      attributeName="transform"
      type="rotate"
      from="0 20 20"
      to="360 20 20"
      dur="4s"
      repeatCount="indefinite"
    ></animateTransform>
  </rect>
</svg>

attributeName is pinned to transform, gradientTransform, and patternTransform — the only three those elements need. Any other target is dropped, so an animation cannot rewrite an href after the compile has checked it. <animate> and <set> take a free-form attributeName by design, which is why they are not allowed at all.

Left out on purpose: <script>, <foreignObject>, <use>, <image>, <feImage>, <a>, <animate>, and <set>. Each is a way to reach out of the graphic — into script, into HTML, or into another document. Every filter primitive that only computes from its inputs is allowed; <feImage> is the one that loads a document, so it is the one that is not. Inside an <svg> they render as the text they were written as, the same way a non-allowlisted HTML tag does.

References stay inside the document. fill="url(#bars)" resolves against the graphic, while fill="url(https://example.com/x)" is dropped, and href is accepted only on textPath and mpath, and only as a #fragment. on* attributes fail the compile and style is sanitized the same way it is in HTML.

Text inside <text> and <tspan> renders as written — Markdown does not apply there. An SVG element written outside a graphic, like a bare <path> in prose, stays literal text. Inside an <svg>, SVG wins over a registered component of the same name, and a component tag written there fails the compile rather than rendering into the graphic.

Images can use Markdown or HTML syntax:

![Build result](screenshots/result.png 'Completed build')

<img src="screenshots/result.png" alt="Build result" width="960" loading="lazy">

Relative, http:, https:, and supported data:image/* sources are accepted. HTML images allow alt, title, width, height, loading, decoding, and class; event handlers and unsafe URL schemes are dropped.

Use src when the source should live next to the HTML, in either form:

<script type="text/htmdx" src="./artifact.mdx"></script>
<htmdx-code src="./artifact.mdx"></htmdx-code>

Module API:

import { compile, compileDocument, register } from '@wix/htmdx';

register();
const rendered = compile('# Title');

Full-document layouts are selected by frontmatter or host options. Host options win:

---
layout: blank
---

# Source-order canvas
compile(source, { layout: 'blank' });
register({ layout: 'blank' });
compileDocument(source, { layout: 'blank' });

Omitting layout uses default, preserving the existing document chrome and automatic ## section grouping. creator-kit is a built-in alias for default, so an artifact can pin the chrome it was authored against by name. blank omits the hero, sticky header, navigation, and grouping while retaining the stable root, catalog, theme, and Tailwind. Htmdx and compileToReact() remain content-only React entrypoints; use compileDocument(source).element for the selected full-document layout.

Trusted hosts register custom React layouts with explicit frontmatter-backed slots:

window.Htmdx.registerLayout({
  name: 'decision',
  slots: {
    eyebrow: { from: 'project' },
    byline: { from: 'owner' },
    status: { from: 'phase' },
  },
  Component: ({ children, slots }) =>
    window.Htmdx.React.createElement('main', null, slots.eyebrow, children),
});

The slots record contains only declared keys; missing fields resolve to undefined, and raw frontmatter is not passed. Names collide case-insensitively, built-in names cannot be replaced, and unknown selected names fail clearly.

Extension API. Trusted host code can contribute React components and theme CSS from an inline or external script:

<script
  src="https://cdn.jsdelivr.net/npm/@wix/htmdx@<exact-version>/dist/browser.js"
  defer
></script>
<script>
  window.addEventListener('htmdx:ready', () => {
    const { createElement } = window.Htmdx.React;

    window.Htmdx.registerComponent({
      name: 'ProductCard',
      purpose: 'Group product details in a card.',
      example: '<ProductCard>Product details.</ProductCard>',
      body: 'htmdx',
      Component: (props) => createElement('aside', { className: 'product-card' }, props.children),
    });

    window.Htmdx.registerTheme({
      id: 'product',
      css: `.product-card { border: 1px solid var(--border); padding: 16px; }`,
    });
  });
</script>

Extension code is host-owned and explicit. The HTMDX source remains declarative; unknown capitalized tags fail compilation until registered.

Tailwind utilities work in registered components by default — the runtime injects Tailwind's browser compiler, so className values compile on the fly.

The runtime injects Tailwind's browser compiler before rendering hosts:

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4" defer></script>

Hosts can disable it or point at a local mirror:

window.Htmdx.register({ tailwind: false });
window.Htmdx.register({ tailwind: { src: './tailwind-browser.js' } });

Use the browser compiler for portable artifacts and prototypes. Production hosts that need a compiled CSS pipeline can disable it and provide their own CSS with registerTheme.

Diagrams

A fenced code block tagged mermaid renders as a diagram:

```mermaid
flowchart LR
  Source --> Runtime --> Diagram
```

Mermaid is not bundled — it is several times the size of the whole runtime — so the first diagram on a page fetches it from a CDN, the same trade the Tailwind compiler makes. compile() stays synchronous: it emits the fence, and the browser upgrades it in place. An artifact that never reaches a browser, or one where the fetch fails, still shows the diagram source.

Hosts can disable it or point at a local mirror:

window.Htmdx.register({ mermaid: false });
window.Htmdx.register({ mermaid: { src: './mermaid.esm.min.mjs' } });

The rendered SVG does not go into the DOM as markup. It is re-parsed and vetted against the same allowlist authored SVG passes, so <foreignObject>, <script>, on*, and references to other documents cannot reach the page no matter what the library emits. Mermaid runs at securityLevel: 'strict' with htmlLabels off, and those keys are locked against an in-source %%{init: ...}%% directive, so a click directive in a diagram binds nothing. Mermaid's own stylesheet is lifted out of the graphic and filtered the same way a style attribute is. A diagram that fails any of this keeps its fence text on the page and says why in the console.

React runtime (MDX minus JavaScript)

htmdx renders through React everywhere: Built-ins and the shadcn/ui pack are bundled as complete component definitions. Global registration and per-render extensions accept the same definitions, and names cannot replace bundled or registered definitions. The source stays declarative data: component tags, nested composition, and declared attribute props work; imports, exports, brace expressions, event handlers, and function props are rejected by design.

The standard runtime script gives an artifact the full catalog:

<script
  src="https://cdn.jsdelivr.net/npm/@wix/htmdx@<exact-version>/dist/browser.js"
  defer
></script>
<!-- prettier-ignore -->
<script type="text/htmdx">
# Q3 Report

<Card class="max-w-xl">
  <CardHeader>
    <CardTitle>Revenue</CardTitle>
    <CardDescription>Audited quarterly numbers</CardDescription>
  </CardHeader>
  <CardContent>
    Revenue grew **12%** quarter over quarter.
    <Badge variant="secondary">audited</Badge>
  </CardContent>
</Card>

<Tabs defaultValue="summary">
  <TabsList>
    <TabsTrigger value="summary">Summary</TabsTrigger>
    <TabsTrigger value="details">Details</TabsTrigger>
  </TabsList>
  <TabsContent value="summary">Topline numbers.</TabsContent>
  <TabsContent value="details">Full cost breakdown.</TabsContent>
</Tabs>
</script>

dist/browser.js bundles React, the built-in catalog (ExecutiveSummary, MetricStrip, charts, ...), the shadcn/ui pack (Card, Badge, Button, Tabs, Accordion), and the shadcn theme, including the static-render path that powers compile(). It stays under 160KB gzip: the build measures the written bundle against build/bundle-budget.json and fails when it grows past the ceiling, so the figure cannot drift without a reviewed change to the budget.

Authoring htmdx source instead of rendered markup is measurably cheaper for agents: the full single-file artifact is about 4.3x smaller in tokens than the same artifact as compiled HTML, and 2-3x smaller than hand-written HTML+Tailwind, with edits cheaper in the same range. Reproducible benchmark in bench/RESULTS.md (yarn bench).

React host apps use the module entries instead (react/react-dom are optional peer dependencies):

import { Htmdx } from '@wix/htmdx/react';
import type { HtmdxComponent } from '@wix/htmdx/components';
import * as builtins from '@wix/htmdx/components/builtins';
import * as shadcn from '@wix/htmdx/components/shadcn';

const MyChart = {
  name: 'MyChart',
  purpose: 'Show a custom chart.',
  example: '<MyChart>Quarterly results.</MyChart>',
  body: 'htmdx',
  Component: MyChartView,
} satisfies HtmdxComponent;

<Htmdx
  source={artifactSource}
  definitions={[...Object.values(builtins), ...Object.values(shadcn), MyChart]}
/>;

Definitions are available from @wix/htmdx/components, @wix/htmdx/components/builtins, and @wix/htmdx/components/shadcn. Component-specific attributes form an allowlist and parse by their declared string, number, boolean, or json type. Every component also accepts class, id, aria-*, and data-*. Well-formed HTMDX bodies are parsed as XML, so camelCase names such as defaultValue stay intact; malformed bodies fall back to HTML parsing.

Security note: the React runtime runs the registered component code with agent-authored props (compile() can still emit a static HTML snapshot of the same tree). Components are host-owned and whitelisted; the source still cannot express code, only data.

Validating source

compile() stops at the first failure. validate() reports every independent problem at once, each anchored to a position in the source:

import { validate } from '@wix/htmdx';

for (const { line, column, severity, code, message } of validate(source)) {
  console.log(`${line}:${column} ${severity} ${code} — ${message}`);
}
// 3:1  error    unknown-component — unknown component <Nope>
// 5:10 error    unknown-prop — unknown prop "tone" for <Callout>
// 9:1  warning  image-missing-alt — image has no alt text

Positions are 1-based line/column plus a 0-based offset and length, so editors and language servers can underline the exact span. An empty array means the source is clean. Like compile(), this needs a DOM (a browser or jsdom).

To run these checks over files from a terminal or CI, see htmdx lint, which adds the findings that only apply to a whole artifact.

Command line

This package ships an htmdx bin, so npx runs it without an install. Every command answers from the runtime doing the answering, so pinning the invocation to the version an artifact declares gets you the behavior that artifact ships:

npx @wix/[email protected] lint docs/*.htmdx --strict
npx @wix/[email protected] compile report.htmdx --out report-body.html
npx @wix/[email protected] components Callout
npx @wix/[email protected] skill

| Command | Description | | ------------------- | --------------------------------------------------------- | | lint <files...> | Report problems. validate is an alias for the same run. | | compile <file> | Print the htmdx-app markup. | | components [name] | List the catalog, or describe one component. | | skill [topic] | Print the authoring guidance shipped with this runtime. |

Exit codes are 0 clean, 1 problems found, and 2 could not run.

htmdx lint

Runs everything validate() reports, plus two findings that only exist once source is embedded in a page: unpinned-runtime (the runtime <script> has no pinned version, so a future release can change the artifact) and runtime-version-mismatch (the artifact pins a version other than the one linting it). That is why the command is lint rather than validate — it is a superset of the API call, checked against a whole file rather than a string. validate is accepted as an alias for anyone who reaches for that name first.

| Option | Description | | ------------------------- | -------------------------------- | | --format <pretty\|json> | Output format. Default pretty. | | --strict | Treat warnings as failures. |

It accepts an HTML artifact — the source comes from its <script type="text/htmdx"> block and positions are reported against the artifact — or a bare source file.

invalid-html-nesting comes from React, which remembers which nesting warnings it has already logged in module state no API resets. Linting many files in one run reports each distinct violation once, on the first file that has it; lint a file on its own to see all of them.

htmdx compile

Prints what compile() returns to a JS caller, for pipelines that want the markup without running the runtime in a browser: a build step, a readable diff in review, or a server that renders once and serves the result.

| Option | Description | | ------------------ | ------------------------------------------- | | -o, --out <file> | Write to a file instead of stdout. | | --layout <name> | Document layout, same names as frontmatter. |

It reads a source file or an artifact, the same way lint does. A source the runtime rejects exits 1 with the compile error on stderr.

The output is the htmdx-app markup, not a standalone page — it carries the class names but not the theme, so serving it still means loading the styles the browser bundle injects.

htmdx components

Prints the component manifest built next to the bin, so the catalog is the one that version renders. With no argument it lists every component grouped by source; with a name it prints that component's purpose, body mode, props, and canonical example.

$ npx @wix/htmdx components Foldout
Foldout

A collapsible panel: a titled header that expands on click to reveal flexible content (text, tables, charts, or any nested component). Collapsed by default; stack multiple for a group.

body: htmdx  source: built-in

props:
  title: string
    The header text shown in the summary row. Supports inline markdown.
  open: boolean (default false)
    Render the panel expanded on load. Defaults to collapsed.

example:
  <Foldout title="Additional details">
  Any content — text, a table, a chart, or any nested component.
  </Foldout>

A name that does not match exits 1 and suggests the closest entries, by substring and by edit distance, so a typo or a half-remembered name still lands: unknown component "Calout"; did you mean Callout?. --format json prints the manifest entry, or the whole manifest when no name is given — the shape to read before writing a document rather than guessing at prop names.

Agent guidance

The same bin prints the authoring guidance that ships with this runtime, so an agent reads the contract, component grammar, and verification steps for the exact version an artifact pins instead of a copy that drifts:

npx @wix/[email protected] skill                 # authoring guidance
npx @wix/[email protected] skill --list          # available topics
npx @wix/[email protected] skill components      # body grammar per component
npx @wix/[email protected] skill --full          # every topic in one stream
npx @wix/[email protected] skill starter > brief.html

--json returns { runtime, topics: [{ name, description, content }] }. An unknown topic exits 2 and names the valid ones. The topic files live in skill/ in this package.

Testing documents

@wix/htmdx/testing covers the two things a consumer's test suite needs: get the source out of a shipped artifact, and snapshot it.

import { extractSource, snapshot } from '@wix/htmdx/testing';

const source = extractSource(readFileSync('report.html', 'utf8'));

expect(snapshot(source)).toMatchInlineSnapshot(`
  markdown "# Report"
  <Callout>
    text "Ship it."
`);

snapshot() defaults to mode: 'structure' — the component tree as written, so upgrading the runtime does not churn every snapshot. Pass mode: 'html' to snapshot the rendered markup instead. Either mode throws if the source has errors, rather than recording the breakage as expected output.