@marsidev/react-turnstile

Cloudflare Turnstile integration for React.

basic-setup

210 linesSource

Install and render your first Turnstile widget with required configuration. Activate this skill when starting a new project with react-turnstile or when implementing basic CAPTCHA protection for the first time.

Basic Setup

Install and render a Cloudflare Turnstile widget in your React application.

Installation

sh
# npm
npm install @marsidev/react-turnstile

# pnpm
pnpm add @marsidev/react-turnstile

# yarn
yarn add @marsidev/react-turnstile

# bun
bun add @marsidev/react-turnstile

Get Your Site Key

Before using Turnstile, you need a site key from Cloudflare:

  1. Go to the Cloudflare Turnstile dashboard
  2. Create a new widget
  3. Copy your site key (starts with 0x or 1x for testing)

Note: Site keys are NOT secret. They are safe to include in client-side code.

Basic Usage

tsx
import { Turnstile } from '@marsidev/react-turnstile'

export default function LoginForm() {
  return (
    <form>
      <input type="email" placeholder="Email" />
      <input type="password" placeholder="Password" />

      {/* Basic Turnstile widget */}
      <Turnstile siteKey="YOUR_SITE_KEY" />

      <button type="submit">Login</button>
    </form>
  )
}

Widget Sizes

Choose the size that fits your layout:

SizeDimensionsBest For
normal (default)300×65pxMost forms, standard layouts
compact150×140pxMobile, tight spaces
flexible100% width × 65pxResponsive layouts
invisible0×0pxHidden widgets (requires Invisible widget type from Cloudflare)
tsx
// Compact size for mobile
<Turnstile
  siteKey="YOUR_SITE_KEY"
  options={{ size: 'compact' }}
/>

// Flexible width
<Turnstile
  siteKey="YOUR_SITE_KEY"
  options={{ size: 'flexible' }}
/>

Handling the Success Token

Use the onSuccess callback to receive the verification token:

tsx
import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'

export default function LoginForm() {
  const [token, setToken] = useState<string | null>(null)

  return (
    <form>
      <input type="email" placeholder="Email" />
      <input type="password" placeholder="Password" />

      <Turnstile
        siteKey="YOUR_SITE_KEY"
        onSuccess={(token) => setToken(token)}
      />

      <button type="submit" disabled={!token}>
        Login
      </button>
    </form>
  )
}

Complete Form Example

tsx
'use client' // For Next.js App Router
import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'

export default function ContactForm() {
  const [token, setToken] = useState<string | null>(null)
  const [isSubmitting, setIsSubmitting] = useState(false)

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    if (!token) return

    setIsSubmitting(true)

    // Send token to your server for validation
    const response = await fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token, /* other form data */ })
    })

    if (response.ok) {
      // Handle success
    }

    setIsSubmitting(false)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="name" placeholder="Name" required />
      <input type="email" name="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />

      <Turnstile
        siteKey="YOUR_SITE_KEY"
        onSuccess={setToken}
      />

      <button type="submit" disabled={!token || isSubmitting}>
        {isSubmitting ? 'Sending...' : 'Send Message'}
      </button>
    </form>
  )
}

Common Mistakes

❌ Using Invisible Size Without Invisible Widget Type

The invisible size is only for the Invisible widget type from Cloudflare. Using it with a normal widget shows nothing.

Wrong:

tsx
// Shows nothing if widget type is not "Invisible"
<Turnstile siteKey="xxx" options={{ size: 'invisible' }} />

Correct:

tsx
// Use visible sizes for normal widgets
<Turnstile siteKey="xxx" options={{ size: 'normal' }} />
<Turnstile siteKey="xxx" options={{ size: 'compact' }} />
<Turnstile siteKey="xxx" options={{ size: 'flexible' }} />

Next Steps

Testing During Development

Use Cloudflare's test keys that always pass validation:

  • Site key: 1x00000000000000000000AA
  • Secret key: 1x0000000000000000000000000000000AA

See: https://developers.cloudflare.com/turnstile/troubleshooting/testing/