Plain HTML
mountly’s most distinctive use case is plain HTML hosts — CMS pages, partner integrations, static sites. No bundler. No SPA. Use custom tags or declarative islands plus one host script tag.
That makes it a strong fit for legacy modernization: you can keep the existing host page and add richer features incrementally instead of rewriting the whole app.
The minimum host (recommended)
Section titled “The minimum host (recommended)”<div data-mountly-island='{"schemaVersion":1,"id":"signup","moduleId":"signup-card","trigger":"idle","props":{"plan":"pro"}}'> <a href="/signup">Sign up (fallback)</a></div>
<script type="module" src="/packages/mountly/dist/host-entry.js" data-mountly-host data-mountly-loaders='{"signup-card":"https://cdn.jsdelivr.net/npm/signup-card@1/dist/index.js"}'></script>host-entry.js auto-mounts all data-mountly-island payloads on the page. Keep meaningful fallback HTML inside each island so content remains usable if JavaScript is disabled.
Programmatic host (advanced)
Section titled “Programmatic host (advanced)”<!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, registerCustomElement, defineMountlyFeature, } from 'mountly';
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>What installRuntime does
Section titled “What installRuntime does”It writes a <script type="importmap"> to <head> mapping 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; 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:
- 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. - First call wins. Calling
installRuntimetwice with different URLs warns and keeps the first set.
See installRuntime for the full reference.
Declarative usage with custom tags
Section titled “Declarative usage with custom tags”If the host is a CMS or a static templating engine where you’d rather drop in HTML, use the custom element aliases:
<signup-card trigger="hover" props='{"plan":"pro"}'></signup-card><payment-breakdown trigger="idle" props='{"invoiceId":"inv_123"}'></payment-breakdown>
<script type="module"> import { defineMountlyFeature } from 'mountly';
defineMountlyFeature('/widgets/dist/index.js');</script>defineMountlyFeature(source) scans the page, defines alias tags, and loads matching named exports from the shared bundle (signup-card → signupCard). If the shared bundle exports a default widget, Mountly uses that as the fallback.
Use prefix when the host needs namespaced custom tags:
<acme-signup-card trigger="hover" props='{"plan":"pro"}'></acme-signup-card>
<script type="module"> import { defineMountlyFeature } from 'mountly';
defineMountlyFeature({ source: '/widgets/dist/index.js', prefix: 'acme', });</script>The tag is prefixed, but the module ID and named export stay unprefixed (signup-card → signupCard).
When widgets are split into separate files, restrict the modules and derive URLs from a base path:
<signup-card trigger="hover" props='{"plan":"pro"}'></signup-card>
<script type="module"> import { defineMountlyFeature } from 'mountly';
defineMountlyFeature({ baseUrl: '/widgets', modules: ['signup-card'], });</script>This loads /widgets/signup-card/dist/index.js and ignores other custom tags on the page. That is the byte-control path.
You can still use the wrapper tag directly when a module ID cannot be a browser custom element name:
<mountly-feature module-id="signup" trigger="hover" props='{"plan":"pro"}'></mountly-feature>Browser custom elements must include a hyphen, so <signup> is not valid.
Multi-widget pages
Section titled “Multi-widget pages”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 ~45 KB gz copy of React, ~5 KB gz per widget. See Distribution.
Diagnostic errors
Section titled “Diagnostic errors”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.
Runnable example
Section titled “Runnable example”The repo’s examples/plain-html directory is the canonical reference. It serves both a self-contained host and a shared-React host on the same domain so you can compare network panels side by side.
cd examples/plain-html && pnpm dev