Captcha
The bot check of a public form: Cloudflare Turnstile that shows nothing to a normal visitor, a framed box on the tokens only when a challenge needs them, and an accessible error with a retry.
import { Captcha, CaptchaDisclosure } from "@krizaka/ui/captcha";Web only — React, from @krizaka/ui/captcha. Beta: its API may still change in a minor version.
- Mobile · n/a. Turnstile is a web widget; a phone app proves itself with its own session.
Why it is here. krizaka.com's contact form and the blog's newsletter form (the public forms, behind Cloudflare Turnstile).
When to use
- A public form anyone can post (contact, newsletter, sign-up) that the server verifies with Turnstile's siteverify.
- When the check must not weigh on the page: it runs in the background and appears only if Cloudflare asks.
When not to use
- A signed-in action: the session already proves who acts.
- A confirmation of a destructive action.Use instead: Confirm button
Installation
Install
npm install @krizaka/ui @krizaka/tailwind @krizaka/tokens tailwindcssStyles
@import "tailwindcss";
@import "@krizaka/tailwind";
@import "@krizaka/ui/tailwind.css";Import
import { Captcha, CaptchaDisclosure } from "@krizaka/ui/captcha";Examples
A form with its check
The submit waits for the token; the disclosure sits under it.
The disclosure line
« Protected by Cloudflare Turnstile · Privacy · Terms », in English or French.
Props
Captcha
The bot check of a public form, Cloudflare Turnstile that stays out of the way: managed mode with
appearance: "interaction-only", so a normal visitor sees nothing — the check runs in the background and onToken
fires. Only when Cloudflare needs the visitor does a framed box appear, on the tokens (radius, border, theme).
If the check cannot run, an alert with a retry button replaces it. Pair it with CaptchaDisclosure under the submit.
| Prop | Type | Default | Description |
|---|---|---|---|
onTokenrequired | (token: string | null) => void | — | The token once the check passes; null when it expires, fails or the theme redraws the widget. |
siteKeyrequired | string | — | The widget's public site key. Empty → nothing is rendered and no token is asked (local dev, tests). |
className | string | — | Classes added to the root. |
labels | Partial<CaptchaLabels> | — | Words to say otherwise; the rest comes from the locale. |
locale | string | en | The language of the widget and of the words: "en", "fr"… |
theme | "auto" | "light" | "dark" | auto | auto follows the page (html.light / data-theme, redrawn when it flips). |
CaptchaDisclosure
« Protected by Cloudflare Turnstile · Privacy · Terms »: the line Cloudflare's terms ask for under the submit button.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Classes added to the root. |
labels | Partial<CaptchaLabels> | — | Words to say otherwise; the rest comes from the locale. |
locale | string | en | The language of the line: "en", "fr"… |
privacyHref | string | https://www.cloudflare.com/privacypolicy/ | The privacy link; Cloudflare's privacy policy by default. |
termsHref | string | https://www.cloudflare.com/website-terms/ | The terms link; Cloudflare's website terms by default. |
Accessibility
- The widget sits in a group named by
labels.group; a failure is announced as an alert, with a button to try again. - The disclosure links open in a new tab with
rel="noopener noreferrer".
Keyboard
| Keys | Action |
|---|---|
| Tab | Reaches the Cloudflare challenge when it shows, and the Try again button on error. |
Best practices
- Verify the token on the server and fail closed: a missing or refused token is a 400, whatever the browser says.
- Put
CaptchaDisclosureunder the submit button: Cloudflare's terms ask for the privacy and terms links when the widget is hidden. - Disable the submit while
onTokenhas not given a token, and remount the component (key) after a refused token. - Pass the site's locale; the theme follows the page (
html.lightordata-theme) and redraws when it flips.
Related components
Generated from the code of @krizaka/ui 3.63.1: its meta.ts, its examples and its types.Edit this documentation on GitHub