COMPONENT · PROVIDER
ActivationGuardProvider
Provides one guard to descendant components through Svelte context and attaches it to the
rendered subtree after the first render. When root is omitted, the provider
creates a display: contents wrapper; pass an element or document as
root to attach to an existing boundary.
Changing a guard configuration option recreates the guard and resets in-flight interaction observations. Keep configuration stable during a press; callback changes are picked up without recreating the guard.
<script lang="ts">
import { ActivationGuardProvider, type Decision } from '@sparshlabs/svelte'
import ApplicationShell from './ApplicationShell.svelte'
function handleDecision(decision: Decision) {
if (!decision.allowed) {
console.info('Suspicious activation:', decision.reason)
}
}
</script>
<ActivationGuardProvider mode="enforce" cooldownMs={500} onDecision={handleDecision}>
<ApplicationShell />
</ActivationGuardProvider> The guard is created in an effect, so it never runs during server-side rendering. The provider works unchanged in SvelteKit and attaches once the page hydrates.
Props
| Prop | Type | Default / behavior |
|---|---|---|
children | Snippet | Subtree to observe (the component's content). |
mode | Mode | Partial<Record<PolicyId, Mode>> | 'enforce'. Unspecified policies in a mode map remain enforced. |
policies | PolicyId[] | All four policies by default. |
cooldownMs | number | 500. Age policy 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. Matching native click window. |
onDecision | (decision: Decision) => void | Called for allowed and suspicious decisions. |
onSuspect | (decision: Decision) => void | Called when a policy verdict is suspicious, 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 custom callback is set. |
blockAnimationMs | number | 300. Default shake duration. |
root | Element | Document | Optional explicit DOM root. |
HOOK · ELEMENT FEEDBACK
useActivationGuard()
Call it in a descendant component's <script>. It returns an attachment
and an isGuarded accessor that becomes true when the element is actually
blocked.
<script lang="ts">
import { useActivationGuard } from '@sparshlabs/svelte'
const { ref, isGuarded } = useActivationGuard()
</script>
<button {@attach ref} type="button">Save draft</button>
{#if isGuarded()}
<p role="status">The screen changed. Try again.</p>
{/if} - Attach the returned
refto the element:<button {@attach ref}>. isGuardedis a reactive accessor; call it (isGuarded()) in markup,$derived, or$effect. It resets after an allowed activation. Destructuring the result keeps it reactive.- Like
getContext, it must be called while the component initialises, not inside an event handler. - Without a provider ancestor, the hook is a no-op and does not throw.
COMPONENT · SCOPED GUARD
ActivationGuard
This is an alias of ActivationGuardProvider with the same props. A nested
instance installs a separate guard on its subtree; options are not merged with an ancestor.
<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
Customize the blocked animation
onBlocked runs after cancellation and receives the interactive element. A
callback exception is ignored so it cannot affect the guard’s decision.
<script lang="ts">
import { ActivationGuardProvider } from '@sparshlabs/svelte'
import ApplicationShell from './ApplicationShell.svelte'
function animateBlocked(element: Element) {
element.animate(
[
{ transform: 'scale(1)' },
{ transform: 'scale(0.96)' },
{ transform: 'scale(1)' },
],
{ duration: 220, easing: 'ease-out' },
)
}
</script>
<ActivationGuardProvider onBlocked={animateBlocked}>
<ApplicationShell />
</ActivationGuardProvider> RELATED TYPES
Decision and mode
Mode, PolicyId, and Decision are re-exported from
@sparshlabs/svelte. A decision’s allowed field records the policy verdict;
enforced says whether it actually stopped the activation. See
Core concepts for policy behavior and accessibility details.
import type { InputKind, Phase, PolicyId } from '@sparshlabs/svelte'
import type { Attachment } from 'svelte/attachments'
export interface UseActivationGuardResult {
ref: Attachment<Element>
isGuarded: () => boolean
}
export interface Decision {
allowed: boolean
enforced: boolean
policy?: PolicyId
reason?: string
event: { kind: InputKind; phase: Phase }
target: {
identity: unknown
ageMs: number
guardKey?: string
}
}