react-turnstile-widget
v2.0.0
Published
A lightweight React wrapper for [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/), supporting both form-based and standalone usage patterns with server-side token verification.
Maintainers
Readme
react-turnstile-widget
A lightweight React wrapper for Cloudflare Turnstile, supporting both form-based and standalone usage patterns with server-side token verification.
Features
- 🧩 Easy-to-use
<TurnstileWidget />React component - 🔄 Custom hook (
useTurnstileWidget) for more control - 🔐
verifyTurnstileToken(token, secretKey)helper for server-side validation - 📝 Works inside forms (automatic hidden field) AND standalone
- ⚙️ Optional
action,cdata, andonSuccessprops - ✅ Written in TypeScript
Installation
pnpm install react-turnstile-widgetHow It Works
The Turnstile widget provides verification tokens in two ways:
- Form Integration: Automatically injects a hidden form field named
cf-turnstile-response - Direct Callback: Passes the token directly to your
onSuccesscallback
This dual approach supports both traditional form submissions and modern component-based patterns.
Usage
🆕 Standalone Usage (New in v2.0)
Perfect for login buttons, API calls, or any non-form interactions:
import { TurnstileWidget } from "react-turnstile-widget";
function LoginComponent() {
const [turnstileToken, setTurnstileToken] = useState<string>("");
const handleLogin = async () => {
if (!turnstileToken) {
alert("Please complete CAPTCHA");
return;
}
const response = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: "[email protected]",
password: "password123",
turnstileToken, // Use token directly
}),
});
};
return (
<div>
<input placeholder="Username" />
<input placeholder="Password" type="password" />
<TurnstileWidget
siteKey="your-site-key"
onSuccess={(token) => setTurnstileToken(token)} // Token provided directly
/>
<button onClick={handleLogin} disabled={!turnstileToken}>
Login
</button>
</div>
);
}📝 Form Integration (Enhanced in v2.0)
Traditional form usage with the added benefit of direct token access:
import {
TurnstileWidget,
TURNSTILE_RESPONSE_PROP,
} from "react-turnstile-widget";
function ContactForm() {
const [captchaCompleted, setCaptchaCompleted] = useState(false);
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
// Option 1: Extract from form data (traditional approach)
const turnstileToken = formData.get(TURNSTILE_RESPONSE_PROP) as string;
// Send to your API for verification
fetch("/api/contact", {
method: "POST",
body: JSON.stringify({
name: formData.get("name"),
email: formData.get("email"),
turnstileToken,
}),
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Your name" required />
<input name="email" type="email" placeholder="Your email" required />
<TurnstileWidget
siteKey="your-site-key"
onSuccess={(token) => {
console.log("CAPTCHA completed with token:", token);
setCaptchaCompleted(true);
// Option 2: You now have direct access to the token here too!
}}
/>
<button type="submit" disabled={!captchaCompleted}>
Submit
</button>
</form>
);
}🎨 Advanced Configuration
<TurnstileWidget
siteKey="your-site-key"
theme="light"
action="submit-form"
cdata="user-123"
onSuccess={(token) => {
console.log("Captcha completed with token:", token);
// Handle token - store in state, send to API, etc.
}}
/>Props:
| Prop | Type | Description |
| ----------- | ----------------------------- | -------------------------------------------------------- |
| siteKey | string | Required. Your Cloudflare Turnstile site key. |
| theme | 'light' \| 'dark' \| 'auto' | Optional. Default is auto. |
| action | string | Optional. Used to describe the context of the challenge. |
| cdata | string | Optional. Custom metadata (visible in dashboard logs). |
| onSuccess | (token: string) => void | Optional. Called with verification token when completed. |
🔧 Manual Usage (Hook)
import { useTurnstile } from "react-turnstile-widget";
function CustomCaptcha({ siteKey }: { siteKey: string }) {
const { ref } = useTurnstile({
siteKey,
onSuccess: (token) => {
console.log("Token received:", token);
},
});
return <div ref={ref} />;
}Server-side Verification
Use the exported helper in your backend handler or API route:
import { verifyTurnstileToken } from "react-turnstile-widget";
// Example API route (Next.js)
export async function POST(request: Request) {
const body = await request.json();
const { turnstileToken, ...otherData } = body;
// Verify the Turnstile token
const isValid = await verifyTurnstileToken(
turnstileToken,
process.env.TURNSTILE_SECRET_KEY!
);
if (!isValid) {
return Response.json(
{ error: "CAPTCHA verification failed" },
{ status: 400 }
);
}
// Process the request
return Response.json({ success: true });
}Alternative: Extract from FormData
If you're working with FormData directly on the server:
import { TURNSTILE_RESPONSE_PROP } from "react-turnstile-widget";
const formData = await request.formData();
const turnstileToken = formData.get(TURNSTILE_RESPONSE_PROP) as string;
const isValid = await verifyTurnstileToken(turnstileToken, secretKey);Constants
Available constants for your convenience:
import {
TURNSTILE_API_URL, // Cloudflare's Turnstile script URL
TURNSTILE_VERIFY_URL, // Server verification endpoint
TURNSTILE_RESPONSE_PROP, // "cf-turnstile-response" - form field name
} from "react-turnstile-widget";Real-World Examples
Button-Triggered Verification
function VerifyButton() {
const [token, setToken] = useState<string>("");
const [isVerifying, setIsVerifying] = useState(false);
const handleVerify = async () => {
if (!token) return;
setIsVerifying(true);
try {
const response = await fetch("/api/verify-action", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ turnstileToken: token }),
});
if (response.ok) {
alert("Verification successful!");
}
} finally {
setIsVerifying(false);
}
};
return (
<div className="space-y-4">
<TurnstileWidget
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
onSuccess={setToken}
/>
<button
onClick={handleVerify}
disabled={!token || isVerifying}
className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
>
{isVerifying ? "Verifying..." : "Verify Action"}
</button>
</div>
);
}Multi-Step Form with Validation
function MultiStepForm() {
const [step, setStep] = useState(1);
const [turnstileToken, setTurnstileToken] = useState<string>("");
const canProceed = step === 1 || (step === 2 && turnstileToken);
return (
<div>
{step === 1 && (
<div>
<h2>Step 1: Basic Info</h2>
<input placeholder="Name" />
<input placeholder="Email" />
<button onClick={() => setStep(2)}>Next</button>
</div>
)}
{step === 2 && (
<div>
<h2>Step 2: Verification</h2>
<TurnstileWidget
siteKey="your-site-key"
onSuccess={(token) => {
setTurnstileToken(token);
console.log("Ready to submit!");
}}
/>
<button onClick={() => setStep(3)} disabled={!canProceed}>
Complete
</button>
</div>
)}
</div>
);
}Migration from v1.x
For users upgrading from v1.x, see the v2.0.0 release notes for migration guidance.
License
MIT
