Skip to content

Plain HTML

mountly runs on plain HTML hosts: CMS pages, partner integrations, static sites. No bundler, no SPA. You drop in custom tags or declarative islands plus one host script tag.

That fits legacy modernization. Keep the existing host page and add richer features one piece at a time instead of rewriting the whole app.

host.html
<div
data-mountly="https://cdn.jsdelivr.net/npm/signup-card@1/dist/index.js"
data-on="idle"
data-props='{"plan":"pro"}'
>
<a href="/signup">Sign up (fallback)</a>
</div>
<script type="module" src="https://cdn.jsdelivr.net/npm/mountly/dist/auto.js"></script>

auto.js is 2.3 KB gzipped and wires every data-mountly element on the page, including ones added later, by a CMS widget or an htmx swap. Keep meaningful fallback HTML inside each island so the content stays usable if JavaScript never arrives.

Prefer short names over URLs? Put the map on the script tag:

<div data-mountly="signup-card" data-on="idle"></div>
<script
type="module"
src="https://cdn.jsdelivr.net/npm/mountly/dist/auto.js"
data-mountly-urls='{"signup-card":"https://cdn.jsdelivr.net/npm/signup-card@1/dist/index.js"}'
></script>
host.html
<!DOCTYPE html>
<html>
<head>
<script type="module">
import { installRuntime } from "https://cdn.jsdelivr.net/npm/mountly@0.1/dist/index.js";
installRuntime({
react: "https://esm.sh/react@18",
reactDom: "https://esm.sh/react-dom@18",
reactDomClient: "https://esm.sh/react-dom@18/client",
});
</script>
<script type="importmap">
{
"imports": {
"signup-card": "https://cdn.jsdelivr.net/npm/signup-card@1/dist/peer.js"
}
}
</script>
</head>
<body>
<button id="cta">Sign up</button>
<script type="module">
import { createOnDemandFeature } from "mountly/feature";
import { registerCustomElement, defineMountlyFeature } from "mountly/elements";
const signup = createOnDemandFeature({
moduleId: "signup-card",
loadModule: () => import("signup-card"),
render: ({ mod, container, props }) => mod.mount(container, props),
});
signup.attach({
trigger: document.getElementById("cta"),
preloadOn: "hover",
activateOn: "click",
});
</script>
</body>
</html>

It writes a <script type="importmap"> to <head> that maps react, react/jsx-runtime, react-dom, and react-dom/client to the URLs you pass. The widget’s peer build imports react as a bare specifier, and the import map resolves that bare specifier to the URL.

installRuntime does not map mountly/* subpaths. If your host imports subpaths (for example mountly/attach, mountly/elements, mountly/shadow, mountly/assets, mountly/adapter), add those entries explicitly in your import map.

Two rules:

  1. Call it before any module imports start resolving. The first inline <script type="module"> is fine; later ones, after other modules have started, are too late and will warn.
  2. First call wins. Calling installRuntime twice with different URLs warns and keeps the first set.

See installRuntime for the full reference.

If you are publishing components for a page you do not control, build them as a script-tag component library. The consuming page needs one script tag and a typed element. No import map, no registration call:

<script type="module" src="https://ui.acme.com/payments/1.2.0/embed.js"></script>
<acme-payments-summary balance="1250" currency="GBP"></acme-payments-summary>

A host that composes several independently-deployed verticals and wants them to share one framework instance uses the <mountly-feature> element instead, driven by its manifest.

For more than one framework widget, use peer builds and one import map for shared framework dependencies:

<script type="module">
import { installRuntime } from "https://cdn.jsdelivr.net/npm/mountly@0.1/dist/index.js";
installRuntime({
react: "https://esm.sh/react@18",
reactDom: "https://esm.sh/react-dom@18",
reactDomClient: "https://esm.sh/react-dom@18/client",
});
</script>
<script type="importmap">
{
"imports": {
"mountly": "https://cdn.jsdelivr.net/npm/mountly@0.1/dist/index.js",
"mountly/attach": "https://cdn.jsdelivr.net/npm/mountly@0.1/dist/attach.js",
"mountly/elements": "https://cdn.jsdelivr.net/npm/mountly@0.1/dist/elements.js",
"mountly/shadow": "https://cdn.jsdelivr.net/npm/mountly@0.1/dist/shadow.js",
"mountly/assets": "https://cdn.jsdelivr.net/npm/mountly@0.1/dist/assets.js",
"mountly/adapter": "https://cdn.jsdelivr.net/npm/mountly@0.1/dist/adapter.js",
"signup-card": "https://cdn.jsdelivr.net/npm/signup-card@1/dist/peer.js",
"payment-breakdown": "https://cdn.jsdelivr.net/npm/payment-breakdown@1/dist/peer.js",
"image-lightbox": "https://cdn.jsdelivr.net/npm/image-lightbox@1/dist/peer.js"
}
}
</script>

One ~59 KB gz copy of React, ~3 KB gz per widget. See Distribution.

If a widget fails to load with “Failed to fetch dynamically imported module” or “Failed to resolve module specifier”, the runtime wraps the error with a hint:

[mountly] loadModule for "signup-card" failed to resolve.
If you're in plain HTML, check that your <script type="importmap"> maps the bare specifier
(e.g. { "imports": { "signup-card": "/path/to/signup-card/dist/index.js" } })
and that installRuntime() runs before any module imports.

When you see that, check the import map and the order of <script> tags.

Live: self-contained host · shared React host

Source lives in docs/examples/plain-html. To run locally:

5175/docs/examples/plain-html/
cd docs/examples/plain-html && pnpm dev