DEVELOPER DOCUMENTATION

React API reference

Provider, hook, scoped wrapper, callbacks, and shared guard options for React applications.

COMPONENT · PROVIDER

ActivationGuardProvider

Installs one guard on a DOM subtree and provides guarded-state subscriptions to descendant hooks. Without root, the provider adds a display: contents wrapper that does not affect normal layout. Mount one provider around the smallest stable subtree that includes the controls you want to protect.

Changing a guard configuration option tears down and recreates the guard, resetting its in-flight interaction observations. Keep configuration stable during a press; changing a callback function alone does not recreate it.

import {
  ActivationGuardProvider,
  type Decision,
} from '@sparshlabs/react'

function handleDecision(decision: Decision) {
  if (!decision.allowed) {
    showTryAgainMessage(decision.reason)
  }
}

export function App() {
  return (
    <ActivationGuardProvider
      mode="enforce"
      cooldownMs={500}
      onDecision={handleDecision}
    >
      <ApplicationRoutes />
    </ActivationGuardProvider>
  )
}

Props

PropTypeDefault / behavior
childrenReactNodeSubtree to observe.
modeMode | Partial<Record<PolicyId, Mode>>'enforce'. A partial map leaves unspecified policies enforced.
policiesPolicyId[]All four policies: age, continuity, semantics, doubleFire.
cooldownMsnumber500. Age threshold in milliseconds.
rectThresholdPxnumber4. Continuity movement tolerance.
refractoryMsnumber300. DoubleFire recency window.
semanticsTextCheckbooleanfalse. Also compare target text.
clickSuppressMsnumber500. Window for matching a native follow-up click.
onDecision(decision: Decision) => voidCalled for every decision, allowed or blocked.
onSuspect(decision: Decision) => voidCalled when allowed is false, including report mode.
onBlocked(element: Element) => voidCalled after an enforced activation is canceled. Replaces the default shake.
blockAnimationbooleantrue. Enables the default shake when no onBlocked callback is set.
blockAnimationMsnumber300. Default shake duration.
rootElement | DocumentOptional explicit root; avoids creating the provider wrapper.

Callback distinction: onSuspect means a policy found an unsafe verdict; report mode still lets that activation through. onBlocked runs only after an enforced activation is actually canceled.

HOOK · ELEMENT FEEDBACK

useActivationGuard()

Returns a callback ref and a boolean for the most recent activation on that element. Attach the ref to a native DOM element or a component that forwards a ref to one. The state is event-driven; it updates when the guard reports a decision for the element.

import { useActivationGuard } from '@sparshlabs/react'

function SaveButton() {
  const { ref, isGuarded } = useActivationGuard()

  return (
    <>
      <button ref={ref} type="button">
        Save draft
      </button>
      {isGuarded && (
        <p role="status">The screen changed. Try again.</p>
      )}
    </>
  )
}
  • ref has the type (node: Element | null) => void.
  • isGuarded is true only after an enforced block; a subsequent allowed activation clears it.
  • Outside a provider, the hook is a no-op and does not throw.
  • Use a live region such as role="status" to explain a block and invite another attempt.

COMPONENT · SCOPED PROVIDER

ActivationGuard

A convenience wrapper with the same props as ActivationGuardProvider. It installs a separate guard scoped to its subtree; it does not inherit or merge options from an ancestor provider. Use it when a region needs an independent mode or policy configuration.

<ActivationGuard mode={{ age: 'report' }}>
  <FrequentlyUpdatedPanel />
</ActivationGuard>

Nesting providers: each provider installs its own capture-phase listeners and policy pipeline — nothing is merged or inherited from an ancestor. When providers overlap the same DOM, the outermost one sees pointer/click/key events first and, if it blocks, the inner provider never sees that interaction at all. This is exactly how <ActivationGuard> is meant to be used for a scoped override, but accidentally wrapping the same subtree with two top-level ActivationGuardProviders is usually a mistake. sparsh logs a console.warn whenever a new guard's root overlaps one that's already active, so you can tell the two cases apart — ignore it for an intentional scoped override, fix it otherwise.

BLOCK FEEDBACK

Provide a custom animation

The callback receives the blocked interactive element after sparsh cancels the activation. When it is supplied, sparsh does not also run its default shake. Callback errors are ignored so visual feedback cannot interfere with the block.

import { ActivationGuardProvider } from '@sparshlabs/react'

export function App() {
  return (
    <ActivationGuardProvider
      onBlocked={(element) => {
        element.animate(
          [
            { transform: 'scale(1)' },
            { transform: 'scale(0.96)' },
            { transform: 'scale(1)' },
          ],
          { duration: 220, easing: 'ease-out' },
        )
      }}
    >
      <ApplicationRoutes />
    </ActivationGuardProvider>
  )
}

RELATED TYPES

Decision and mode

Mode is 'report' | 'enforce'. Decision includes allowed (the policy verdict), enforced (whether the activation was blocked), optional policy and reason, input event details, and a target snapshot summary. The binding re-exports these types from @sparshlabs/core.

import type { InputKind, Phase } from '@sparshlabs/react'

export type Mode = 'report' | 'enforce'
export type PolicyId = 'age' | 'continuity' | 'semantics' | 'doubleFire'

export interface Decision {
  allowed: boolean
  enforced: boolean
  policy?: PolicyId
  reason?: string
  event: { kind: InputKind; phase: Phase }
  target: {
    identity: unknown
    ageMs: number
    guardKey?: string
  }
}

export interface UseActivationGuardResult {
  ref: (node: Element | null) => void
  isGuarded: boolean
}

For policy behavior and accessibility details, see Core concepts.