Skip to content

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.

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-cardsignupCard). 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>
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">.

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');

Set on <mountly-feature> or an alias tag such as <signup-card>:

AttributeTypeDefaultNotes
module-idstringrequiredMust match a registerCustomElement factory.
trigger"hover" · "click" · "focus" · "viewport" · "idle" · "media" · "url-change""click"High-level trigger preset.
trigger-delaynumber mstrigger defaultHover delay override.
preload-on"hover" · "viewport" · "idle" · "media" · "false"mapped from triggerExplicit attach().preloadOn override.
activate-on"click" · "hover" · "focus" · "viewport" · "idle" · "media" · "url-change"mapped from triggerExplicit attach().activateOn override.
preload-media-querystringRequired with preload-on="media".
activate-media-querystringRequired with activate-on="media" or trigger="media".
idle-timeoutnumber msUsed by idle triggers.
viewport-root-marginstring"0px"Forwarded to IntersectionObserver.
url-eventscomma list (popstate,hashchange,pushstate,replacestate)all fourUsed with activate-on="url-change" / trigger="url-change".
data-urlstringIf set, the element fetches JSON from this URL as loadData.
data-method"GET" · "POST" etc."GET"Used with data-url.
propsJSON string{}Passed to the feature’s render().
mount-selectorCSS selectorselfRender 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.

The element translates trigger into the appropriate preloadOn / activateOn pair:

triggerpreloadOnactivateOn
"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.

<mountly-feature> can match Astro client hydration timing semantics:

  • client:idletrigger="idle" (optionally idle-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 call feature.mount(...) programmatically

Astro-specific compile-time/server directives (client:only, server:defer, set:html, is:inline, etc.) are outside mountly’s runtime scope.

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' }));

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.

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;
});