Documentation Index
Fetch the complete documentation index at: https://mintlify.com/marsidev/react-turnstile/llms.txt
Use this file to discover all available pages before exploring further.
Explore practical examples of React Turnstile in various scenarios and frameworks.
Live Demo
Check out the live demo application:
React Turnstile Demo
Interactive demo with multiple examples and configurations
Example Repository
All examples are available in the GitHub repository:
Demo Source Code
Browse the complete demo implementation built with Next.js
Basic Implementation
Simple form with Turnstile protection:
'use client'
import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'
export default function ContactForm() {
const [token, setToken] = useState<string | null>(null)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!token) {
alert('Please complete the security check')
return
}
const formData = new FormData(e.target as HTMLFormElement)
await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify({
email: formData.get('email'),
message: formData.get('message'),
token,
}),
})
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<Turnstile
siteKey="1x00000000000000000000AA"
onSuccess={setToken}
/>
<button type="submit">Send Message</button>
</form>
)
}
Render multiple Turnstile widgets on the same page:
'use client'
import { Turnstile } from '@marsidev/react-turnstile'
import { useRef } from 'react'
export default function MultipleWidgets() {
const widget1Ref = useRef(null)
const widget2Ref = useRef(null)
return (
<div>
<section>
<h2>Login Form</h2>
<Turnstile
ref={widget1Ref}
id="widget-1"
siteKey="1x00000000000000000000AA"
options={{ size: 'normal' }}
/>
</section>
<section>
<h2>Newsletter Signup</h2>
<Turnstile
ref={widget2Ref}
id="widget-2"
siteKey="1x00000000000000000000AA"
options={{ size: 'compact' }}
/>
</section>
</div>
)
}
Manual Script Injection
Control when and how the Turnstile script loads:
'use client'
import Script from 'next/script'
import { Turnstile, SCRIPT_URL, DEFAULT_SCRIPT_ID } from '@marsidev/react-turnstile'
export default function ManualInjection() {
return (
<>
<Script
id={DEFAULT_SCRIPT_ID}
src={SCRIPT_URL}
strategy="beforeInteractive"
/>
<Turnstile
siteKey="1x00000000000000000000AA"
injectScript={false}
/>
</>
)
}
Custom Script Props
Customize the injected script with CSP nonce and other options:
'use client'
import { Turnstile } from '@marsidev/react-turnstile'
export default function CustomScriptProps() {
return (
<Turnstile
siteKey="1x00000000000000000000AA"
scriptOptions={{
nonce: 'your-csp-nonce',
appendTo: 'head',
defer: true,
async: true,
crossOrigin: 'anonymous',
}}
/>
)
}
Complete form with validation and submission:
'use client'
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
import { useRef, useState, type FormEvent } from 'react'
export default function CompleteForm() {
const turnstileRef = useRef<TurnstileInstance>(null)
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
setError(null)
setIsSubmitting(true)
const token = turnstileRef.current?.getResponse()
if (!token) {
setError('Please complete the security check')
setIsSubmitting(false)
return
}
try {
const formData = new FormData(e.currentTarget)
const response = await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: formData.get('name'),
email: formData.get('email'),
token,
}),
})
if (!response.ok) {
throw new Error('Submission failed')
}
alert('Form submitted successfully!')
e.currentTarget.reset()
turnstileRef.current?.reset()
} catch (err) {
setError('Failed to submit form. Please try again.')
turnstileRef.current?.reset()
} finally {
setIsSubmitting(false)
}
}
return (
<form onSubmit={handleSubmit}>
<input
name="name"
type="text"
placeholder="Name"
required
disabled={isSubmitting}
/>
<input
name="email"
type="email"
placeholder="Email"
required
disabled={isSubmitting}
/>
<Turnstile
ref={turnstileRef}
siteKey="1x00000000000000000000AA"
onError={() => setError('Security check failed. Please try again.')}
/>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
)
}
Server-Side Verification
Next.js API route for server-side token validation:
// app/api/verify/route.ts
import type { TurnstileServerValidationResponse } from '@marsidev/react-turnstile'
const VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'
export async function POST(request: Request) {
const { token } = await request.json()
if (!token) {
return Response.json(
{ success: false, error: 'Token is required' },
{ status: 400 }
)
}
const response = await fetch(VERIFY_URL, {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
secret: process.env.TURNSTILE_SECRET_KEY!,
response: token,
}),
})
const data: TurnstileServerValidationResponse = await response.json()
if (!data.success) {
return Response.json(
{ success: false, errors: data['error-codes'] },
{ status: 400 }
)
}
return Response.json({
success: true,
challenge_ts: data.challenge_ts,
hostname: data.hostname,
})
}
Theme Switching
Dynamic theme switching based on user preference:
'use client'
import { Turnstile } from '@marsidev/react-turnstile'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
export default function ThemedTurnstile() {
const { resolvedTheme } = useTheme()
const [key, setKey] = useState(0)
// Force re-render when theme changes
useEffect(() => {
setKey(prev => prev + 1)
}, [resolvedTheme])
return (
<Turnstile
key={key}
siteKey="1x00000000000000000000AA"
options={{
theme: resolvedTheme === 'dark' ? 'dark' : 'light',
}}
/>
)
}
Invisible Challenge
Invisible widget that triggers on form submission:
'use client'
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
import { useRef, useState } from 'react'
export default function InvisibleChallenge() {
const turnstileRef = useRef<TurnstileInstance>(null)
const [isProcessing, setIsProcessing] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsProcessing(true)
// Trigger the invisible challenge
turnstileRef.current?.execute()
// Get the token (will wait for challenge to complete)
try {
const token = await turnstileRef.current?.getResponsePromise()
if (token) {
await submitForm(token)
}
} catch (error) {
console.error('Challenge failed:', error)
} finally {
setIsProcessing(false)
}
}
const submitForm = async (token: string) => {
// Your form submission logic
console.log('Submitting with token:', token)
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<Turnstile
ref={turnstileRef}
siteKey="1x00000000000000000000AA"
options={{
size: 'invisible',
execution: 'execute',
}}
/>
<button type="submit" disabled={isProcessing}>
{isProcessing ? 'Processing...' : 'Submit'}
</button>
</form>
)
}
Multi-Page Application
Persist Turnstile state across page navigation:
// components/TurnstileProvider.tsx
'use client'
import { createContext, useContext, useState, type ReactNode } from 'react'
interface TurnstileContextType {
token: string | null
setToken: (token: string | null) => void
isVerified: boolean
}
const TurnstileContext = createContext<TurnstileContextType | undefined>(undefined)
export function TurnstileProvider({ children }: { children: ReactNode }) {
const [token, setToken] = useState<string | null>(null)
const isVerified = !!token
return (
<TurnstileContext.Provider value={{ token, setToken, isVerified }}>
{children}
</TurnstileContext.Provider>
)
}
export function useTurnstile() {
const context = useContext(TurnstileContext)
if (!context) {
throw new Error('useTurnstile must be used within TurnstileProvider')
}
return context
}
// pages/step1.tsx
import { Turnstile } from '@marsidev/react-turnstile'
import { useTurnstile } from './TurnstileProvider'
import { useRouter } from 'next/navigation'
export default function Step1() {
const { setToken } = useTurnstile()
const router = useRouter()
const handleSuccess = (token: string) => {
setToken(token)
router.push('/step2')
}
return (
<div>
<h1>Step 1: Verification</h1>
<Turnstile
siteKey="1x00000000000000000000AA"
onSuccess={handleSuccess}
/>
</div>
)
}
// pages/step2.tsx
import { useTurnstile } from './TurnstileProvider'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
export default function Step2() {
const { token, isVerified } = useTurnstile()
const router = useRouter()
useEffect(() => {
if (!isVerified) {
router.push('/step1')
}
}, [isVerified, router])
if (!isVerified) return null
return (
<div>
<h1>Step 2: Complete Form</h1>
<p>Token: {token}</p>
{/* Your form here */}
</div>
)
}
More Examples
For more examples, check out the demo application:
- Basic Usage: Simple widget implementation
- Manual Script Injection: Custom script loading
- Multiple Widgets: Multiple widgets on one page
- Custom Script Props: CSP and nonce configuration
- Theme Customization: Light, dark, and auto themes
- Size Variants: Normal, compact, flexible sizes
- Language Support: Multi-language examples
Visit the live demo to see all examples in action.