Skip to content
Utilities

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

PropTypeDefaultDescription
namestring"company_website"The field name the server reads. Anything non-empty is a bot.
...input propsReact.ComponentProps<'input'>—Forwarded to the input. `type` is fixed at text.