Custom element
import { defineMountlyFeature } from "mountly";Custom elements are the declarative path for HTML-first hosts: CMS pages, marketing sites, partner integrations, and static pages that should not own widget lifecycle code.
Wire it up
Section titled “Wire it up”For a shared widget bundle, put the tags in HTML and call defineMountlyFeature(source) once:
<signup-card trigger="viewport" 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>By default, Mountly scans the page, defines alias tags such as <signup-card>, registers them as module IDs, and loads matching named exports from the shared bundle (signup-card → signupCard). If the named export is missing, it falls back to the bundle’s default export.
You can still use the wrapper tag directly:
<mountly-feature module-id="signup-card" trigger="viewport" props='{"plan":"pro"}'></mountly-feature>defineMountlyFeature(input?)
Section titled “defineMountlyFeature(input?)”function defineMountlyFeature(input?: string | DefineMountlyFeatureOptions): void;
interface DefineMountlyFeatureOptions { tagName?: string; source?: string; moduleUrl?: string; // alias for source modules?: FeatureModuleManifest; aliases?: boolean | Record<string, string>; prefix?: string; scan?: boolean; auto?: boolean; // alias for scan baseUrl?: string; resolveModuleUrl?: (moduleId: string) => string;}Common forms:
defineMountlyFeature();Defines <mountly-feature> and scans for tags that already provide their own module-url, src, or props.moduleUrl.
defineMountlyFeature("/widgets/dist/index.js");Uses one shared bundle for all discovered alias tags.
defineMountlyFeature({ source: "/widgets/dist/index.js", prefix: "acme",});Defines tags such as <acme-signup-card> while keeping the module ID as signup-card and the shared-bundle export as signupCard. Use this when a CMS or host page needs a namespace to avoid tag-name collisions.
defineMountlyFeature({ baseUrl: "/widgets", modules: ["signup-card", "payment-breakdown"],});Registers only listed modules and derives URLs such as /widgets/signup-card/dist/index.js. This is the byte-control path when each widget has its own bundle.
defineMountlyFeature({ modules: { "signup-card": "/widgets/signup-card.js", "payment-breakdown": "/widgets/payment-breakdown.js", },});Registers explicit per-widget URLs.
For internal module IDs that cannot be browser custom element names, use an alias map:
defineMountlyFeature({ modules: { signup: "/widgets/signup.js", }, aliases: { "signup-card": "signup", },});Browsers require custom element tag names to contain a hyphen, so <signup> cannot be defined. The direct wrapper tag still works for non-hyphen IDs: <mountly-feature module-id="signup">.
registerCustomElement(moduleId, factory)
Section titled “registerCustomElement(moduleId, factory)”function registerCustomElement( moduleId: string, factory: () => OnDemandFeature | Promise<OnDemandFeature>,): void;Low-level escape hatch. Maps a module-id attribute value to a factory that returns a feature. Use this when the feature needs custom loadData, analytics hooks, or bespoke attach() behavior.
To unregister:
unregisterCustomElement("signup-card");Element attributes
Section titled “Element attributes”Set on <mountly-feature> or an alias tag such as <signup-card>:
| Attribute | Type | Default | Notes |
|---|---|---|---|
module-id |
string | required | Must match a registerCustomElement factory. |
trigger |
"hover" · "click" · "focus" · "viewport" · "idle" · "media" · "url-change" |
"click" |
High-level trigger preset. |
trigger-delay |
number ms | trigger default | Hover delay override. |
preload-on |
"hover" · "viewport" · "idle" · "media" · "false" |
mapped from trigger |
Explicit attach().preloadOn override. |
activate-on |
"click" · "hover" · "focus" · "viewport" · "idle" · "media" · "url-change" |
mapped from trigger |
Explicit attach().activateOn override. |
preload-media-query |
string | — | Required with preload-on="media". |
activate-media-query |
string | — | Required with activate-on="media" or trigger="media". |
idle-timeout |
number ms | — | Used by idle triggers. |
viewport-root-margin |
string | "0px" |
Forwarded to IntersectionObserver. |
url-events |
comma list (popstate,hashchange,pushstate,replacestate) |
all four | Used with activate-on="url-change" / trigger="url-change". |
data-url |
string | — | If set, the element fetches JSON from this URL as loadData. |
data-method |
"GET" · "POST" etc. |
"GET" |
Used with data-url. |
props |
JSON string | {} |
Passed to the feature’s render(). |
mount-selector |
CSS selector | self | Render target other than the element itself. |
Alias tags infer module-id from the tag name, so <signup-card> does not need the module-id attribute.
Trigger → attach mapping
Section titled “Trigger → attach mapping”The element translates trigger into the appropriate preloadOn / activateOn pair:
trigger |
preloadOn |
activateOn |
|---|---|---|
"hover" |
"hover" |
"hover" |
"focus" |
false |
"focus" |
"viewport" |
"viewport" |
"viewport" |
"idle" |
"idle" |
"click" |
"media" |
false |
"media" |
"url-change" |
false |
"url-change" |
"click" (default) |
false |
"click" |
If you need different combinations, use feature.attach(...) directly.
Astro parity notes
Section titled “Astro parity notes”<mountly-feature> can match Astro client hydration timing semantics:
client:idle→trigger="idle"(optionallyidle-timeout)client:visible={{ rootMargin }}→trigger="viewport"+viewport-root-margin="..."client:media="(query)"→trigger="media"+activate-media-query="(query)"client:load/ immediate mount → skip triggers and callfeature.mount(...)programmatically
Astro-specific compile-time/server directives (client:only, server:defer, set:html, is:inline, etc.) are outside mountly’s runtime scope.
Live prop updates
Section titled “Live prop updates”Setting the props attribute on a connected element triggers feature.update(container, parsedProps), which preserves framework-internal state when the widget supports it.
const el = document.querySelector("mountly-feature");el.setAttribute("props", JSON.stringify({ plan: "pro" }));Errors
Section titled “Errors”If a module-id has no registered factory:
[mountly] <mountly-feature module-id="x"> has no registered factory.Call registerCustomElement("x", () => yourFeature) before the element connects.Currently registered: "signup-card", "payment-breakdown".The element stays inert until the factory is registered. With alias tags, check that defineMountlyFeature(...) ran after the markup exists, or pass a modules list for tags created later.
Lifecycle hooks
Section titled “Lifecycle hooks”The element does not expose onMount / onUnmount itself. For those, attach analytics handlers via the feature’s onAnalyticsEvent or wire them inside the factory:
registerCustomElement("signup-card", () => { const feature = createOnDemandFeature({ … }); // Hook into all mounts of this feature, regardless of trigger. onAnalyticsEvent("signup-card", (e) => analytics.track(e.phase)); return feature;});Related
Section titled “Related”- Custom element concept: the explainer.
createOnDemandFeature: what the factory returns.