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.

| Attribute | Type | Default | Notes | | ---------------------- | ---------------------------------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------- | | module-id | string | inferred on alias tags | Must 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-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 render(). Live-updates via update(). | | mount-selector | CSS selector | self | Render 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.).