Browse documentation

Workspace documentation

Accessible lead forms with layered spam checks

Build a form people can complete and correct. Try the working HTML, then add the server checks that a browser cannot enforce.

Start with the form a person uses

Give each field a visible label. Mark required fields in words and in HTML. Ask for the contact details you need to reply, and preserve the visitor's answers when validation fails. A placeholder disappears as someone types, so it cannot replace a label.

The downloadable form demonstrates labeled inputs, an error summary, links to invalid fields and a hidden honeypot. Entries stay in the browser: it has no delivery endpoint, analytics or storage. Open it, submit it empty, then correct the fields using only your keyboard.

Keep the honeypot out of the person's way

A honeypot is an extra field that should remain empty. Some automated submissions fill every field, giving your server a signal to examine. Hiding it with the hidden attribute keeps it out of the layout and accessibility tree; tabindex prevents a fallback keyboard stop. Do not make it required.

This is a weak signal, not proof that a visitor is a bot. Targeted scripts can leave it empty, and autofill can produce false positives. Route suspicious submissions for review, or let the visitor complete a fresh challenge without retyping their message. Do not quietly discard a customer's request solely because it arrived quickly.

<div hidden>
  <label for="company-site">Leave this field empty</label>
  <input id="company-site" name="companySite"
    type="text" tabindex="-1" autocomplete="off">
</div>

Let a visitor recover from an error

On submission, show a short summary that links to each invalid field. Move focus to that summary, associate each field with its error text using aria-describedby and set aria-invalid only while the error exists. Explain how to fix the value. Color alone does not explain a problem.

A failed request should keep the entered values and give a retry path. A success message should appear only after the server has accepted the request. In the downloadable form, the success text says that fields were checked and nothing was sent.

Enforce abuse controls on the server

Browser checks help people correct mistakes; they do not protect an endpoint. Validate the submitted types and lengths again on the server, and limit request size and rate. If your app serves multiple businesses, resolve the receiving business from trusted routing. Never let an arbitrary hidden tenant ID decide which customer's inbox receives a lead.

Use your framework's origin and CSRF protections, encode submitted text when displaying it and keep email destinations under server control. Give each submission an idempotency key and enforce uniqueness within its receiving business so retries return the original result instead of creating two jobs. Save the lead before attempting downstream notifications, and report delivery failures to the operator.

If you add Cloudflare Turnstile, verify the token with Siteverify on your server. Check success and the expected hostname and action; keep the secret out of browser code. Tokens expire after five minutes and can be used only once, so an expired check needs a fresh token and a clear retry. A rendered widget alone does not validate a request.

Connect your own endpoint

The download checks fields locally. To turn it into a live form, implement a server endpoint first, then replace the local confirmation with a request containing the name, email, message, companySite and an idempotency key. Update the form's Content Security Policy to permit that specific destination. Keep the browser and server field limits aligned.

Choose an explicit response contract: return success only after saving the lead; return field errors for a validation failure, such as HTTP 422; and provide a retry message for a temporary failure, such as HTTP 503. Preserve the fields and the submission's idempotency key when retrying an uncertain result. Generate a new key for a new submission. Never label a network timeout as a successful delivery.

Check the whole path before launch

Try keyboard-only entry, an empty submission, an invalid email and corrected resubmission. Inspect the accessibility tree to confirm the trap is absent. Use a screen reader to check labels, focus and announcements; an automated scan cannot establish that experience.

Test the real server with JavaScript disabled, oversized input, a filled trap, repeated submissions and a failed notification. Confirm that the correct business receives exactly one lead and that retries preserve the visitor's answers. Test autofill and shared-network rate limits before treating a suspicious signal as a rejection.

Keep a small, access-controlled record of rejection reasons and review false positives. Avoid logging complete messages or contact details just to measure spam. No single field or challenge guarantees that a form is spam-free.