HoneypotField
A bait input no person can see or reach, for a form that takes public submissions. A script fills every field it finds; a person fills the ones they are shown. So a request that arrives with this one non-empty came from a script, and the server drops it without a captcha, a cookie or a third party.
There is nothing to look at, which is the point. Tab through the form below: focus goes from the email box straight to the button.
Import
import { HoneypotField } from "@mikenotthepope/substrateui"Reading it on the server
The component is half the pattern. The other half is one line wherever the form is handled: if the field arrived with anything in it, stop. Return the same success the form always returns rather than an error, so a script learns nothing about why it failed.
Give it a name a script will want to fill. company_website is the default for that reason; honeypot is not, because it says what it is.
Why it is off-screen and not hidden
It is positioned off the start edge rather than given display: none. Some scripts skip a field they can tell is hidden, which would defeat the point. Its parent needs a positioning context, so put relative on the form.
It is also not sr-only. That clips the field to a 1px box but leaves it in the layout at its own position, and a zero-size field reads to a script the same way a hidden one does.
Accessibility
The field carries aria-hidden so a screen reader never announces it, and tabIndex={-1}so Tab skips it. Both are required together: jsx-a11y's no-aria-hidden-on-focusable and axe's aria-hidden-focus each read an input as focusable unless its tabindex is negative, so a field with only the first of the two fails the gates it was meant to be invisible to.
It has no label, on purpose. A label would give it a name for a screen reader, and it is meant to have none.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | "company_website" | The field name the server reads. Anything non-empty is a bot. |
...input props | React.ComponentProps<'input'> | — | Forwarded to the input. `type` is fixed at text. |