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
| Prop | Type | Default / behavior |
|---|---|---|
children | ReactNode | Subtree to observe. |
mode | Mode | Partial<Record<PolicyId, Mode>> | 'enforce'. A partial map leaves unspecified policies enforced. |
policies | PolicyId[] | All four policies: age, continuity, semantics, doubleFire. |
cooldownMs | number | 500. Age threshold in milliseconds. |
rectThresholdPx | number | 4. Continuity movement tolerance. |
refractoryMs | number | 300. DoubleFire recency window. |
semanticsTextCheck | boolean | false. Also compare target text. |
clickSuppressMs | number | 500. Window for matching a native follow-up click. |
onDecision | (decision: Decision) => void | Called for every decision, allowed or blocked. |
onSuspect | (decision: Decision) => void | Called when allowed is false, including report mode. |
onBlocked | (element: Element) => void | Called after an enforced activation is canceled. Replaces the default shake. |
blockAnimation | boolean | true. Enables the default shake when no onBlocked callback is set. |
blockAnimationMs | number | 300. Default shake duration. |
root | Element | Document | Optional 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>
)}
</>
)
} refhas the type(node: Element | null) => void.isGuardedis 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.