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.
In one example
Section titled “In one example”<signup-card trigger="viewport" props='{"plan":"pro"}'></signup-card>
<script type="module"> import { defineMountlyFeature } from "mountly/elements";
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.
Wire it once
Section titled “Wire it once”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.
Attributes
Section titled “Attributes”| 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. |
preload-on |
"hover" · "viewport" · "idle" · "media" |
none | Opt-in prefetch. Never implied by trigger. |
activate-on |
"click" · "hover" · "focus" · "viewport" · "idle" · "media" · "url-change" |
trigger |
Activation trigger when it differs from trigger. |
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. |
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 | own [data-mountly-mount] slot |
Render target. Must match a descendant of the element. |
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" },});Live prop updates
Section titled “Live prop updates”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" }));Mount target
Section titled “Mount target”The element renders into a <div data-mountly-mount> it appends to itself. Pass
mount-selector to render into a container you control instead — it has to be a
descendant of the element, so the slot stays inside the element’s own subtree:
<mountly-feature module-id="payment-breakdown" trigger="click" mount-selector=".panel"> <button>Buy</button> <div class="panel"></div></mountly-feature>A selector matching nothing inside the element falls back to the default slot.
To mount into a container elsewhere on the page, use the core’s
data-target, which does fall back to a document-wide lookup.
Unknown module-id
Section titled “Unknown module-id”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.
When to prefer the imperative API
Section titled “When to prefer the imperative API”- You need
onMount/onErrorcallbacks. - 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.
Astro parity scope
Section titled “Astro parity scope”You can replicate Astro timing directives in declarative HTML:
client:idle→trigger="idle"(optionallyidle-timeout)client:visible={{ rootMargin }}→trigger="viewport"+viewport-root-margin="..."client:media→trigger="media"+activate-media-query="(query)"
Hover has no delay attribute; it uses the core’s 100 ms settle. For a different
delay, use the core directly with data-on="hover:300".
Compiler/server directives remain Astro-only (client:only, server:defer, set:html, etc.).