Skip to content

Custom element

For hosts that drop in HTML — CMS pages, marketing sites, partner integrations — declarative is easier to maintain than imperative. mountly ships a custom element for that.

<signup-card trigger="viewport" props='{"plan":"pro"}'></signup-card>
<script type="module">
import { defineMountlyFeature } from 'mountly';
defineMountlyFeature('/widgets/dist/index.js');
</script>

That’s the common path. Mountly scans the page, defines <signup-card>, registers it as a feature, and loads it from the shared source. The element handles attach, mount, prop updates, and unmount on disconnect.

If all widgets live in one bundle, pass that bundle once:

defineMountlyFeature('/widgets/dist/index.js');

If the host page needs namespaced tags, add a prefix:

<acme-signup-card trigger="viewport" props='{"plan":"pro"}'></acme-signup-card>
defineMountlyFeature({
source: '/widgets/dist/index.js',
prefix: 'acme',
});

The prefix only affects the browser tag. Mountly still uses signup-card as the module ID and signupCard as the named export in a shared bundle.

For per-widget bundles, give Mountly a base URL and limit the allowed modules:

defineMountlyFeature({
baseUrl: '/widgets',
modules: ['signup-card', 'payment-breakdown'],
});

That registers only those modules and resolves /widgets/signup-card/dist/index.js, /widgets/payment-breakdown/dist/index.js, and so on. This saves bytes when components are split into separate bundles.

You can also stay explicit:

defineMountlyFeature({
modules: {
'signup-card': '/widgets/signup-card.js',
'payment-breakdown': '/widgets/payment-breakdown.js',
},
});

The lower-level registerCustomElement() API is still available when a feature needs custom loading, data, or analytics wiring.

AttributeTypeDefaultNotes
module-idstringinferred on alias tagsMust match a registered feature. Alias tags use their tag name.
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 render(). Live-updates via update().
mount-selectorCSS selectorselfRender target other than the element itself.

Custom element alias tags must contain a hyphen because the browser requires it. If your internal module ID is signup, use <mountly-feature module-id="signup"> or map a valid alias:

defineMountlyFeature({
modules: { signup: '/widgets/signup.js' },
aliases: { 'signup-card': 'signup' },
});

Change the props attribute and the element calls feature.update(...). If the widget exposes update(container, props) (React adapter does, others can opt in), framework-internal state is preserved. Otherwise it falls back to a re-render in the same container.

const el = document.querySelector('mountly-feature');
el.setAttribute('props', JSON.stringify({ plan: 'enterprise' }));

By default, the widget mounts inside the element itself. To mount into a sibling instead — useful when the trigger element should stay tiny and the widget renders into a pre-positioned container — pass mount-selector:

<button id="cta">Buy</button>
<div id="modal-root"></div>
<mountly-feature
module-id="payment-breakdown"
trigger="click"
mount-selector="#modal-root"
></mountly-feature>

The element’s own DOM stays empty; the widget renders into #modal-root.

If the element connects with a module-id that has no registered factory, you get a console warning listing the registered IDs. The element stays inert until the factory is registered later — useful when the registration is split across code-split chunks.

  • You need onMount / onError callbacks.
  • The trigger element is dynamic (e.g. created in response to user input).
  • You want to wire analytics next to the trigger.

In those cases, call feature.attach({ trigger, mount, … }) directly. The custom element is the ergonomic shortcut for static-HTML hosts; attach() is the full API.

You can replicate Astro timing directives in declarative HTML:

  • client:idletrigger="idle" (optionally idle-timeout)
  • client:visible={{ rootMargin }}trigger="viewport" + viewport-root-margin="..."
  • client:mediatrigger="media" + activate-media-query="(query)"

Compiler/server directives remain Astro-only (client:only, server:defer, set:html, etc.).