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.
In one example
Section titled “In one example”<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.
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. 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.
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. |
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' },});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”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.
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 the factory is registered later — useful 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)"
Compiler/server directives remain Astro-only (client:only, server:defer, set:html, etc.).