DEVELOPER DOCUMENTATION

DOM and vanilla API

Use the browser adapter directly when you do not use React or Vue, or when you need to own the guard lifecycle yourself.

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(): void removes event listeners and disconnects observers. Call it when the guarded surface is torn down.
  • isGuarded(target): boolean reports 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.

OptionTypeDefault / effect
modeMode | Partial<Record<PolicyId, Mode>>'enforce'; partial-map omissions also enforce.
policiesPolicyId[]All policies enabled.
cooldownMsnumber500 ms, Age threshold.
rectThresholdPxnumber4 px, Continuity tolerance.
refractoryMsnumber300 ms, DoubleFire window.
semanticsTextCheckbooleanfalse; opt in to comparing text.
clickSuppressMsnumber500 ms to match a native follow-up click.
blockAnimationbooleantrue; default shake unless onBlocked is provided.
blockAnimationMsnumber300 ms, default shake duration.
onBlocked(element: Element) => voidCustom feedback after actual cancellation; replaces the default shake.
onDecision(decision: Decision) => voidEvery 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.