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>
)
}
Multiple Widgets
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',
}}
/>
)
}
Form Integration
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