Skip to main content

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>
  )
}

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
Visit the live demo to see all examples in action.

Build docs developers (and LLMs) love