Guide 10 · Developer docs

Forms and data capture

Building forms with the Forms module: how a form is put together and placed on a page, validating and branching, presenting it, where submissions are stored, and what happens after one arrives, from jobs and records to CRMs and payments.

L1–L2
Unit 04 · Presenting a form

Captcha

What the Captcha element does, the form setting that keeps failed submissions out of your data, where the captcha belongs, the script conflicts to avoid, and how to diagnose a captcha problem.

Who this is for. The site builder who adds a captcha to a form, and the developer called in when a form stops submitting. Afterwards you can configure the Captcha element so failed attempts stay out of the data, place it where it works, and diagnose a captcha problem.

A captcha issue rarely shows an error, so start in the browser when diagnosing one. Every case described here was reproduced in a browser against a real form.

What the element does

The Captcha element is Google reCAPTCHA v2, in one of two modes chosen on the element:

  • "I am not a robot" Checkbox. The element prints a label, Google's widget, and a hidden helper checkbox marked required, which the widget ticks when it is solved and unticks when the answer expires. The helper is what stops the browser submitting an unsolved form.
  • Invisible reCAPTCHA badge. No widget, only a badge. When Google's script has loaded, the element takes over the form's onsubmit: pressing the button runs the challenge, and only a passed challenge submits the form.

Either way the element prints its own scripts, a small inline one and Google's recaptcha/api.js, both carrying the page's nonce, so the site's script policy allows them with nothing configured. A page with three captchas loads Google's script three times; that works, but it is one more reason to have one captcha per page.

On submit, the answer travels as a field named g-recaptcha-response, and the server checks it with Google. Google accepts an answer once, and for two minutes. The reCAPTCHA keys belong to the platform's environment configuration, set by Core dna, so there is nothing to configure on the site. If the widget shows "Invalid domain for site key", the site's domain is not registered on the platform's key; raise it with Core dna.

Tick "Require completed captcha"

The element alone does not keep a failed submission out of your data. The form's Settings tab has Require completed captcha, and it is off by default:

Setting off (the default)Setting on
A bot submits without solving the captchaThe submission is saved, as incomplete, with every answer it sent, and the uploads of any other step that passed are stored tooNothing is saved. The page shows the captcha error; the API answers 422
A visitor's captcha fails on the first submitThe step comes back with the answers in placeThe step comes back with its fields empty, since nothing was saved to print back
Jobs on CompletedDo not start: the submission is not completeDo not start
The submissions listing and exportsFill with incomplete spamStay clean

So a captcha without the setting stops spam from reaching your jobs, but not your database or your file store. Tick it on every public form that has a captcha, and put the captcha on the last step (below), so that the step a failure empties is the shortest one.

Where it belongs

On the last step, directly above the submit button. Three reasons:

  • Google's answer lasts two minutes. On a JS form, every step's answers, the captcha's included, are sent together when the last step is submitted. A captcha solved on step 1 of a five-minute form has expired by then, and the visitor is on the last step with the error on a step they can no longer see.
  • The answer is used up by the check. On the page-per-step model, a captcha on any step must be solved again after every failed attempt at that step, so put it where there is only one attempt left to make.
  • The visitor solves it at the moment they decide to send, which is when the friction costs least.

Use one captcha per form: the server reads the answer from g-recaptcha-response whatever the element is called. And never replace the element's markup with your own: its scripts look for the ids it prints.

Use checkbox mode on a JS form. Invisible mode takes over the form's submit, and the JS model handles the same submit with its own script. The two collide in exactly the way the next section describes, every time.

The script conflicts that stop a form

Invisible mode assigns form.onsubmit when Google's script finishes loading, and Google's script is loaded async: it runs whenever it arrives. A theme's form bundle is usually loaded with defer, and runs when the page has been parsed. Which of the two runs first depends on the network, so a conflict between them can work on the developer's machine and fail for visitors. Tested against a live form with every submission intercepted:

Your codeWhat happensWhat the visitor sees
Assigns form.onsubmit (for validation), and runs before Google's scriptThe captcha replaces it. Your validation never runsA form that submits without your checks
Assigns form.onsubmit, and runs after Google's scriptIt replaces the captcha. The form posts with an empty answer, and the server returns the captcha errorThe captcha error, every time, with nothing to solve
Adds a submit listener that sends the form with fetchBoth run. Your request goes at once with an empty answer and gets the captcha error; when the visitor solves the challenge, the captcha posts the form natively, a second requestWhatever your code shows for a failure (often nothing), then a page reload
Nothing; Google's script is blocked (an ad blocker, a privacy extension, a company firewall)Invisible: onsubmit is never assigned and the form posts with no answer. Checkbox: the widget never appears, and the required helper checkbox stops the browser submittingInvisible: the captcha error, every time. Checkbox: "Please check this box", pointing at a box behind where the widget should be. Ticking it submits the form, and the server returns the captcha error

The rules that follow from it:

  • Never assign form.onsubmit on a form that has a captcha. Validate with the browser's own constraints (required, pattern, setCustomValidity()), which run before any submit handler and do not compete with one.
  • Never send a captcha form yourself. If a form must submit by script, use the JS model with checkbox mode, and let the platform's script send it.
  • Say what to do when the captcha cannot load. Some visitors will never see Google's widget. Put another route in the form's own copy ("Can't send this form? Email us at…") so that a blocked script costs a visitor a few seconds, not the enquiry.

Diagnosing a form that will not submit

The symptom is no error at all, so start in the browser. Open the page, open the developer tools console, and run:

JavaScript
console.table([...document.querySelectorAll('.g-recaptcha')].map((widget) => {
  const form = widget.closest('form');
  const invisible = widget.dataset.size === 'invisible';
  const answer = form && form.querySelector('[name="g-recaptcha-response"]');
  return {
    id: widget.id,
    mode: invisible ? 'invisible' : 'checkbox',
    googleLoaded: typeof window.grecaptcha !== 'undefined',
    widgetRendered: !!widget.querySelector('iframe'),
    captchaOwnsSubmit: invisible ? String(form && form.onsubmit).includes('grecaptcha') : 'n/a',
    answer: answer && answer.value ? 'present' : 'empty',
  };
}));

Read the row:

  • googleLoaded false: Google's script did not load. Check the Network panel for a blocked recaptcha/api.js, and the console for a security-policy refusal.
  • widgetRendered false in checkbox mode, with googleLoaded true: the widget div was replaced or moved after the script ran, typically by code that re-renders the form's markup.
  • captchaOwnsSubmit false in invisible mode: something replaced the captcha's submit handler. In Chrome, getEventListeners(form) in the console lists the submit handlers and the file each came from.
  • answer empty after you solved it: the answer expired (two minutes), or was used by an earlier attempt.

Then submit once with the Network panel open. A page-per-step form posts to the page itself; look at the request's form data for a non-empty g-recaptcha-response. A JS form posts to /cdna-api/forms/submission/<form id>; a 422 or 207 answer with "ReCAPTCHA challenge has to be resolved" under the captcha's name in validationErrors is this case. Show it to the visitor with the result callback on Validation.

When the browser shows a good answer and the server still refuses it, Google rejected it. The platform records Google's reason in its log (timeout-or-duplicate for an expired or reused answer, invalid-input-response for a malformed one), which Core dna can read for you.

Accessibility

Google's widget is the accessible control, with its own audio challenge. The helper checkbox is not: it has no label, so a screen reader announces an unlabelled required checkbox beside the real one. Hide it from assistive technology once the page has loaded:

JavaScript
document.querySelectorAll('.g-recaptcha + .help-input').forEach((helper) => {
  helper.setAttribute('aria-hidden', 'true');
  helper.tabIndex = -1;
});

It still blocks an unsolved submit, which is its job. And because some visitors cannot complete any captcha, the alternative route above is an accessibility requirement as much as a reliability one. Use a captcha where a bot gains something: registration, contact and enquiry forms. A newsletter sign-up with double opt-in usually does not need one.

Go deeper

Was this page helpful?

Your answer goes to the docs team.