mountly
Your components. Someone else’s page. One script tag.
Publish a React, Vue, or Svelte component as a custom element behind one script tag. The consuming page installs nothing, writes no import map, and calls no init. Its props type is the whole contract.
On demand still means what it always did, one layer in: that script registers every tag and downloads nothing else. A component, its framework and its CSS arrive when one of its elements connects, and not before.
Two other hosts take the same components and a different API: a page you run yourself, where the runtime mounts them on user intent and shares one framework across many; and MCP Apps, where Claude and ChatGPT render them when a tool is called.
Small core + explicit subpaths.
mountly keeps a small top-level core entry and exposes focused subpath imports (for example
mountly/attach, mountly/elements,mountly/shadow,
mountly/assets). In browser import-map hosts, map only the subpaths you use.
One primitive. Several high-value jobs.
You write a component. mountly either builds it into a tag another team drops in, or, on a page you run, loads and mounts it from plain HTML when the page signals intent. Marketing reuse, strangler-fig migration and island UI sit on the second one.
What you keep
- Normal React, Vue, or Svelte components
- Existing CMS, static, legacy, or app host pages
- Current routing, query-string state, and deployment model
- Per-widget ownership by the component team
No new component authoring model. No full app rewrite.
What mountly adds
- HTML tags like
<signup-card> - Activation on idle, viewport, click, hover, media, or URL change
- Optional shadow-DOM style isolation, and CSS loaded with the component
- Module/data caching, teardown, and prop updates
Delivery only. Routing, SSR, rollouts and shared state stay where they are.
Where the primitive pays off.
shared block.verify –render proves each one boots and is accessible before you ship it.Six ways to say “now”.
A trigger is the signal that moves a feature from idle to preload to
mount. Pick the one that matches user intent, or compose your own with a plugin.
requestIdleCallback to preload during quiet moments.One state machine, five states. The same shape on every framework.
Every feature moves through the same sequence regardless of trigger or framework. You hook into any phase, abort in flight, and unmount without leaks.
shadow: true).A namespace and a glob. Or a widget, a feature and a trigger.
Components stay components. Name a namespace, point the build at the files, and you are done with the author side.
import { defineElementsConfig } from "mountly-vite-plugin";
// Every component in the directory becomes a tag: <acme-signup-card>export default defineElementsConfig({ prefix: "acme", elements: "src/elements/*.tsx" });<script type="module" src="https://ui.acme.com/signup/1.2.0/embed.js"></script><acme-signup-card plan="pro" data-mountly-trigger="viewport"></acme-signup-card>On a page you run yourself, the runtime takes over: the adapter wraps a component as a widget, and you wire a feature in code or declare it as an HTML tag.
import { createWidget } from "mountly-react";import SignupCard from "./SignupCard.tsx";import styles from "./SignupCard.css?inline";
// 1. Wrap a component as a framework-agnostic widgetexport default createWidget(SignupCard, { styles });import { createOnDemandFeature } from "mountly/feature";
// 2. Add the on-demand lifecycle around a widgetconst signup = createOnDemandFeature({ moduleId: "signup-card", loadModule: () => import("./signup-card.js"), render: ({ mod, container, props }) => mod.mount(container, props),});
// 3. Attach it to a DOM trigger: preload on hover, mount on clicksignup.attach({ trigger: document.querySelector("#cta")!, preloadOn: "hover", activateOn: "click",});Styling: mountly loads CSS with the module and applies it before render, so no FOUC. Light DOM lets the host’s design system reach in; pass
shadow: truefor full isolation.
Composable with frameworks. Useful where framework boundaries end.
| Approach | Best at | Tradeoff | Complexity |
|---|---|---|---|
| Traditional SPA | One app owns every surface | Poor fit for CMS, legacy, or partner-hosted UI | Low |
| Framework code-splitting | Routes inside a single framework app | Does not standardize HTML drops or host-agnostic lifecycle | Medium |
| Microfrontend orchestrators | Independent apps with org-level ownership | Operationally heavy for component-sized features. Shared config blocks, runtime negotiation, build-time coupling. | High |
| Monorepo + npm libs | Same org, shared CI, lazy routes until clean break | Merge coordination; not for foreign hosts you cannot merge into | Low |
| Import-map widgets (mountly model) | Team-scalable widgets with zero orchestrator | Shared React version agreement; you own CDN/manifest workflow | Low |
| mountly | Components in an HTML page that does not run your framework | An embed bundles its own framework copy. On the runtime you own the CDN, the import map and version pinning. No SSR, no rollout control. | Low |
One runtime, four adapters, an MCP Apps toolchain, one optional design preset.
dev host, build, and verify –render.mountly-reactReact adapter: createWidget(Component, { styles }).mountly-vueVue adapter: createWidget(Component), lightweight createApp per container.mountly-svelteSvelte adapter: auto-detects v4 class API and v5 functional mount/unmount.mountly-tailwindOptional Tailwind v4 design preset. Tokens shared with the runtime.