Skip to content

Spam Protection

Every protection the HTML library has, the React package has too. The difference is where the evidence comes from: React’s own events, not DOM listeners. Nothing to configure; this page explains what is happening so you do not accidentally switch it off.

The form does nothing until a person does something. Five signals open the gate:

SignalSource
focusAny registered input receives focus, or anything inside the form does (capture-phase onFocusCapture).
clickA click inside the form, or submit() being called.
keydownA key pressed inside the form.
touchA touch starts inside the form.
visibleThe form element scrolls into view (an IntersectionObserver on formProps.ref, 10% visible).

The first one to fire opens the server session (idle to initializing) and stamps the moment the visitor started. Until then no request leaves the page, which is why a crawler that never interacts costs you nothing and a script that posts to the API without a session gets nowhere.

At init the server names a decoy field for this form, a plausible-looking name like phone that differs per form. form.honeypotProps carries it once the session is open and <SnipForm> (or <Honeypot>) renders it:

<input type="text" name="phone" value="" tabindex="-1" autocomplete="off" aria-hidden="true" readonly
style="position:absolute; left:-10000px; width:1px; height:1px; opacity:0; overflow:hidden">

Off screen, unfocusable, read only and empty. A human never sees it; a bot that fills every input it finds fills this one, and the server refuses the submission with a 403. The field is sent on every submit as '' and stripped by the server before validation, so it never appears in your submissions.

If you build your own <form> instead of using <SnipForm>, render <Honeypot form={form} /> inside it. Without it the server still accepts the form but you lose the trap.

Alongside the values, each submit carries a small _sig object:

KeyMeaningCollected from
durMilliseconds between the gate opening and submitThe gate timestamp
gtWhat opened the gate: focus, click, keydown, touch, visibleThe gate
kKeystrokesonKeyDown from register()
fFocus eventsonFocus from register()
tDistinct fields touchedonFocus, onChange, setValue
mMouse movedOne passive mousemove listener on window
sPage scrolledOne passive scroll listener on window
wnavigator.webdriverSet by Puppeteer, Playwright, Selenium
pnavigator.plugins.lengthHeadless browsers typically report 0
lnavigator.languages.lengthAutomated browsers often report 0

The server combines these with what it can measure itself, chiefly the time between the session opening and the submit arriving, which the client cannot fake, plus network origin (datacenter ranges, Tor exits). The result is a score and one of four actions.

ActionWhat happens
acceptSaved normally.
flagSaved, marked suspicious for review in the dashboard.
fake_successNot saved. The client receives the normal success response.
rejectNot saved, logged as spam. The client receives the normal success response.

Signals are only as good as the events that feed them. Two rules:

  1. Spread register() whole. Its onFocus and onKeyDown are the f and k counters. An input wired with only onChange reports no focus and no keystrokes, which reads as programmatic filling.
  2. Keep at least one typed input. A form made entirely of selects and checkboxes produces no keystrokes. That is fine on its own, but combine it with a fast submit and it scores worse than a form someone typed into. Custom components driven purely by setValue count as touched fields but produce neither focus nor keystroke events.

Validation happens before scoring, so a real visitor always sees their validation errors regardless of score.

These are server rules, shared with every client and covered in How Forms Work. The package honours them for you:

  • Init and submit carry a signature derived from the form key (or session token) and the UTC date. The package computes it with the same algorithm as the server; a wrong signature is a 403.
  • The session is pinned to the visitor’s connection and lives 600 seconds. A 419 (expired, already used, or connection changed) makes the hook open a fresh session and retry the submit once, invisibly.
  • The field set is frozen at init. A submit or blur-validate that includes an undeclared field is a 419 “Data discrepancy”, which is why fields must list everything.
HTML library@snipform/react
Gate triggersvisible, focus, click, keydown, touch via DOM listenersThe same five, via React events and one observer
HoneypotInjected into the DOMRendered from honeypotProps
SignalsDOM listeners on the host elementregister() handlers plus two passive window listeners
RequiresMarkup the library can scanregister() spread on inputs

Same server, same scorer, same outcome. The only way to weaken it from React is to bypass register().