Skip to content

Custom element

Some hosts drop in HTML rather than write JS: CMS pages, marketing sites, partner integrations. For them, declarative markup is easier to maintain than imperative code. 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 for you.

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. You save bytes when components 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, pass mount-selector. This helps when the trigger element should stay tiny and the widget renders into a pre-positioned container:

<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 you register the factory later, which helps 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.).