Skip to content

Custom triggers

A trigger decides when an island loads and mounts. Eight ship with the core, and adding a ninth takes one line — there is no plugin API to learn, because the trigger table is already the registry.

Each reads as kind or kind:arg, so a trigger never needs a second attribute to hold its argument.

data-on / data-preload Argument Fires when
click The island is clicked
focus Focus lands inside it (focusin, so children count)
hover Delay in ms, default 100 The pointer settles over it
viewport rootMargin, default 0px It intersects the viewport
idle Timeout in ms The browser goes idle (requestIdleCallback)
media A media query The query matches, now or later
url popstate, hashchange, pushState or replaceState
never Only when code calls mount(el)
<button data-mountly="/w.js" data-on="click focus">Any of them fires</button>
<div data-mountly="/w.js" data-on="media:(min-width: 60rem)"></div>
<div data-mountly="/w.js" data-on="viewport:200px"></div>
<div data-mountly="/w.js" data-preload="hover:300"></div>

triggers is a plain object of (element, arg, run) => teardown. Assign to it before mountly() runs.

import { mountly, triggers } from "mountly";
triggers["double-tap"] = (el, windowMs, run) => {
let last = 0;
const handler = () => {
const now = performance.now();
if (now - last < (Number(windowMs) || 300)) run();
last = now;
};
el.addEventListener("pointerup", handler);
return () => el.removeEventListener("pointerup", handler);
};
mountly();
<div data-mountly="/w.js" data-on="double-tap:250"></div>

run may fire many times — the core ignores repeats once the island is mounted (unless data-toggle is set), so a trigger does not need to de-duplicate. Return a teardown that removes whatever you attached; the core calls it when the island is torn down.

An unknown data-on kind is an error, not a silent no-op: the island lands in data-mountly-state="error" and dispatches mountly:error.

mountly/gestures ships swipe, long-press and keyboard-shortcut primitives in the same handler-plus-teardown shape, so wiring one up as a trigger is a forwarding call.

import { triggers } from "mountly";
import { eachSwipe, eachLongPress, eachKeyboard } from "mountly/gestures";
triggers.swipe = (el, direction, run) =>
eachSwipe(el, run, { direction: direction as "up" | "down" | "left" | "right" });
triggers.longpress = (el, duration, run) =>
eachLongPress(el, run, { duration: Number(duration) || undefined });
triggers.hotkey = (_el, key, run) => eachKeyboard(run, { key, meta: true });
<div data-mountly="/drawer.js" data-on="swipe:left"></div>
<div data-mountly="/menu.js" data-on="longpress:600"></div>
<div data-mountly="/palette.js" data-on="hotkey:k"></div>

They are kept out of the core because most pages never need them: the core is 2.3 KB, and importing mountly/gestures is what decides whether swipe detection ships to your users.

Primitive Signature Options
eachSwipe (el, handler, opts?) direction ("left" default), threshold px (40), velocity px/ms (0.3)
eachLongPress (el, handler, opts?) duration ms (500), tolerance px (10)
eachKeyboard (handler, opts) key (required), meta, shift, alt, preventDefault

Each also has a one-shot promise form — onSwipe, onLongPress, onKeyboard — for await-style code outside the island lifecycle.