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) — preserving 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.