ENTRY POINT · @sparshlabs/dom
createGuard(root, options?)
Creates the DOM host and core engine together, installs capture-phase listeners on the supplied root, and returns a handle for querying block state and cleaning up. Prefer the smallest stable root that contains all controls you want guarded.
npm install @sparshlabs/dom import { createGuard } from '@sparshlabs/dom'
const appRoot = document.querySelector('#app')
if (!(appRoot instanceof Element)) {
throw new Error('App root not found')
}
const guard = createGuard(appRoot, {
mode: 'enforce',
onDecision(decision) {
if (decision.enforced) {
console.info('Blocked:', decision.policy, decision.reason)
}
},
})
// Call this when the guarded UI is removed.
guard.destroy() Returned guard
destroy(): voidremoves event listeners and disconnects observers. Call it when the guarded surface is torn down.isGuarded(target): booleanreports whether the target’s latest decision was actually blocked.
Low-level option: createDomHost(root, options) exposes the DOM
implementation of the core Host port. Use it only when you intentionally want to
construct the core engine separately; ordinary applications should use createGuard.
OPTIONS
Guard configuration
createGuard combines core policy options with DOM-host options. React and Vue
providers accept these same options.
| Option | Type | Default / effect |
|---|---|---|
mode | Mode | Partial<Record<PolicyId, Mode>> | 'enforce'; partial-map omissions also enforce. |
policies | PolicyId[] | All policies enabled. |
cooldownMs | number | 500 ms, Age threshold. |
rectThresholdPx | number | 4 px, Continuity tolerance. |
refractoryMs | number | 300 ms, DoubleFire window. |
semanticsTextCheck | boolean | false; opt in to comparing text. |
clickSuppressMs | number | 500 ms to match a native follow-up click. |
blockAnimation | boolean | true; default shake unless onBlocked is provided. |
blockAnimationMs | number | 300 ms, default shake duration. |
onBlocked | (element: Element) => void | Custom feedback after actual cancellation; replaces the default shake. |
onDecision | (decision: Decision) => void | Every decision, in report and enforce mode. |
CUSTOM BLOCK FEEDBACK
Animate only after a real block
onBlocked runs only when an enforced decision has canceled the activation. It
receives the interactive element, and a thrown error is ignored so cosmetic code cannot
affect input handling.
import { createGuard } from '@sparshlabs/dom'
const guard = createGuard(document.body, {
onBlocked(element) {
element.animate(
[
{ transform: 'translateX(0)' },
{ transform: 'translateX(-4px)' },
{ transform: 'translateX(4px)' },
{ transform: 'translateX(0)' },
],
{ duration: 180 },
)
},
}) LIFECYCLE
Always destroy the guard
A guard owns listeners and visibility/age tracking for its root. Keep its lifetime aligned with that UI root. For React and Vue, the provider handles setup and cleanup automatically.
Composing multiple guards: guards never merge or inherit options from one
another — each createGuard call installs its own independent listeners and
policy pipeline. If a guard's root overlaps (is the same as, contains, or is contained by) a
root that already has an active guard, sparsh logs a console.warn so the overlap
doesn't go unnoticed. This is expected and safe for an intentional scoped override (a nested
guard on a sub-region with different options); if it wasn't intentional, make sure each
subtree is only wrapped once.