Skip to content

mountly

v1.0 · nothing for the consumer to install

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.

2.3KBcore entry (gzipped)
7activation triggers
4adapter packages
0SPA required
3frameworks, one page
1build, web + MCP hosts
0x · Runtime shape

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.

01 · Positioning

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.

02 · Use cases

Where the primitive pays off.

01embedsHand a component to another teamPublish it as a custom element behind one script tag. A partner site, a CMS template, or another team’s app writes the tag and installs nothing. The props type is the contract.
02marketingReal product components on campaign pagesReuse checkout, pricing, signup, and calculator components on fast campaign pages without rewriting the CMS host.
03migrationStrangler fig, one widget at a timeReplace legacy surfaces incrementally. Each widget replaces one section. The host never rewrites. When all widgets are done, the legacy is gone.
04multi-teamIndependent widget releasesTeam A ships a payments widget, Team B ships chat, and Team C owns the host. A CDN and manifest provide lightweight microfrontend composition without a Federation shared block.
05cross-frameworkReact + Vue + Svelte on one pageThree teams, three frameworks, one page. Each widget is a bare specifier in the import map. Teams converge on the next architecture at their own pace.
06performanceIntent-paid JavaScriptKeep the page shell light. Load heavy UI when users scroll, click, hover, or match a route. 2.3KB core, widgets on demand.
07mcp appsYour components, rendered by an AI hostShip the same React, Vue, or Svelte component as an MCP Apps view that Claude and ChatGPT render when a tool is called. Views install into the MCP server you already own, and verify –render proves each one boots and is accessible before you ship it.
03 · Triggers

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.

01on intenthoverPreload on mouseenter, mount on click. The default for discoverable controls.
02explicitclickMount on click without preloading. Use when intent is committed, not exploratory.
03accessibilityfocusPreload on focus, mount on commit. Keyboard parity with hover.
04below the foldviewportMount when the element scrolls into view. Configurable threshold.
05predictiveidleUse requestIdleCallback to preload during quiet moments.
06routingurl-changeMount when the URL matches a pattern. Pairs with client-side navigation.
04 · Lifecycle

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.

01idleNothing loaded. Waiting for the trigger.
02preloadModule fetched, data fetch may begin in parallel.
03activateCommitment signal. Finalise data, prepare to mount.
04mountWidget rendered into the container (light DOM by default; shadow root with shadow: true).
05unmountRemoved from DOM. Caches retained for re-mount.
05 · The whole API

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.

vite.config.ts
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" });
someone else's page
<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.

signup-card.ts
import { createWidget } from "mountly-react";
import SignupCard from "./SignupCard.tsx";
import styles from "./SignupCard.css?inline";
// 1. Wrap a component as a framework-agnostic widget
export default createWidget(SignupCard, { styles });
page.ts
import { createOnDemandFeature } from "mountly/feature";
// 2. Add the on-demand lifecycle around a widget
const 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 click
signup.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: true for full isolation.

06 · How it compares

Composable with frameworks. Useful where framework boundaries end.

ApproachBest atTradeoffComplexity
Traditional SPAOne app owns every surfacePoor fit for CMS, legacy, or partner-hosted UILow
Framework code-splittingRoutes inside a single framework appDoes not standardize HTML drops or host-agnostic lifecycleMedium
Microfrontend orchestratorsIndependent apps with org-level ownershipOperationally heavy for component-sized features. Shared config blocks, runtime negotiation, build-time coupling.High
Monorepo + npm libsSame org, shared CI, lazy routes until clean breakMerge coordination; not for foreign hosts you cannot merge intoLow
Import-map widgets (mountly model)Team-scalable widgets with zero orchestratorShared React version agreement; you own CDN/manifest workflowLow
mountlyComponents 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
07 · Packages

One runtime, four adapters, an MCP Apps toolchain, one optional design preset.

mountly · Apache-2.0 · v1.0github.com/jagreehal/mountly

see ./examples in the repo