npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nextjs-turnstile

v1.0.5

Published

Cloudflare Turnstile CAPTCHA integration for Next.js - Simple, stable, and fully typed

Downloads

1,919

Readme

Next.js Turnstile

npm version License npm downloads

A simple, stable, and fully-typed Cloudflare Turnstile CAPTCHA integration for Next.js applications.

Features

  • Simple API - Single <Turnstile> component with sensible defaults
  • Fully Typed - Complete TypeScript support with JSDoc comments
  • Stable - Uses explicit rendering mode for reliable React lifecycle management
  • Imperative API - Control the widget programmatically via ref
  • SSR Safe - Works with Next.js App Router and Pages Router
  • Server Verification - Built-in token verification utility

Installation

npm install nextjs-turnstile

Quick Start

1. Set up environment variables

# .env.local
NEXT_PUBLIC_TURNSTILE_SITE_KEY=your_site_key_here
TURNSTILE_SECRET_KEY=your_secret_key_here

Get your keys from the Cloudflare Dashboard.

2. Add the widget to your form

"use client";

import { Turnstile } from "nextjs-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 CAPTCHA");
      return;
    }

    // Send token to your API for verification
    const response = await fetch("/api/contact", {
      method: "POST",
      body: JSON.stringify({ token, /* ...form data */ }),
    });
    
    // Handle response...
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Your form fields */}
      
      <Turnstile
        onSuccess={setToken}
        onError={() => console.error("Turnstile error")}
        onExpire={() => setToken(null)}
      />
      
      <button type="submit" disabled={!token}>
        Submit
      </button>
    </form>
  );
}

3. Verify the token on your server

// app/api/contact/route.ts (App Router)
import { verifyTurnstile } from "nextjs-turnstile";

export async function POST(request: Request) {
  const { token } = await request.json();

  const isValid = await verifyTurnstile(token);
  
  if (!isValid) {
    return Response.json(
      { error: "CAPTCHA verification failed" },
      { status: 400 }
    );
  }

  // Token is valid, continue with your logic...
  return Response.json({ success: true });
}

Component Props

<Turnstile
  // Site key (falls back to NEXT_PUBLIC_TURNSTILE_SITE_KEY env var)
  siteKey="your-site-key"
  
  // Appearance
  theme="auto"        // "auto" | "light" | "dark"
  size="normal"       // "normal" | "compact" | "flexible"
  appearance="always" // "always" | "execute" | "interaction-only"
  
  // Behavior
  execution="render"       // "render" | "execute"
  refreshExpired="auto"    // "auto" | "manual" | "never"
  refreshTimeout="auto"    // "auto" | "manual" | "never"
  retry="auto"             // "auto" | "never"
  retryInterval={8000}     // Retry interval in ms
  
  // Form integration
  responseFieldName="cf-turnstile-response"  // Name for hidden input, or false to disable
  
  // Analytics
  action="login"           // Custom action identifier (max 32 chars)
  cData="user-123"         // Custom data payload (max 255 chars)
  
  // Accessibility
  tabIndex={0}
  language="auto"          // ISO 639-1 code or "auto"
  
  // Styling
  className="my-turnstile"
  style={{ marginTop: 16 }}
  
  // Feedback
  feedbackEnabled={true}     // Allow Cloudflare feedback on failure (default: true)
  
  // Callbacks
  onSuccess={(token) => {}}  // Called with verification token
  onError={(code) => {}}     // Called on error
  onExpire={() => {}}        // Called when token expires (~5 min)
  onTimeout={() => {}}       // Called on interactive timeout
  onLoad={() => {}}          // Called when widget is ready
  onBeforeInteractive={() => {}}  // Called before interactive challenge
  onAfterInteractive={() => {}}   // Called after interactive challenge
  onUnsupported={() => {}}        // Called if browser not supported
/>

Imperative API (Ref)

Use a ref to control the widget programmatically:

import { Turnstile, TurnstileRef } from "nextjs-turnstile";
import { useRef } from "react";

function MyForm() {
  const turnstileRef = useRef<TurnstileRef>(null);

  const handleReset = () => {
    turnstileRef.current?.reset();
  };

  const handleSubmit = async () => {
    const token = turnstileRef.current?.getResponse();
    
    if (!token) {
      alert("Please complete the CAPTCHA");
      return;
    }

    // Submit form...
  };

  return (
    <form>
      <Turnstile ref={turnstileRef} onSuccess={console.log} />
      
      <button type="button" onClick={handleReset}>
        Reset CAPTCHA
      </button>
      <button type="button" onClick={handleSubmit}>
        Submit
      </button>
    </form>
  );
}

Ref Methods

| Method | Description | |--------|-------------| | reset() | Reset the widget for a new challenge | | remove() | Remove the widget from the page | | getResponse() | Get the current token (or null) | | execute() | Start the challenge (when execution="execute") | | isReady() | Check if the widget is ready | | getWidgetId() | Get the internal Cloudflare widget ID |

Server-Side Verification

verifyTurnstile(token, options?)

Verifies a Turnstile token with Cloudflare's siteverify API. Returns true on success. Throws TurnstileError on failure with an errorCodes array describing what went wrong.

import { verifyTurnstile } from "nextjs-turnstile";

// Basic usage (uses TURNSTILE_SECRET_KEY env var)
const ok = await verifyTurnstile(token);

Error handling

import { verifyTurnstile, TurnstileError } from "nextjs-turnstile";

try {
  await verifyTurnstile(token);
} catch (e) {
  if (e instanceof TurnstileError) {
    console.error(e.errorCodes); // e.g. ["invalid-input-response"]
  }
}

Advanced options

const ok = await verifyTurnstile(token, {
  secretKey: "custom-secret-key",  // Override secret key
  ip: "1.2.3.4",                   // User's IP (auto-detected if omitted)
  headers: request.headers,        // For IP detection in Pages Router
  action: "login",                 // Reject if action doesn't match
  hostname: "example.com",         // Reject if hostname doesn't match
  timeout: 5000,                   // Fetch timeout in ms (default: 10 000)
});

Parameters:

  • token (string): The token from the Turnstile widget
  • options (object, optional):
    • secretKey: Override the default secret key (falls back to TURNSTILE_SECRET_KEY env var)
    • ip: User's IP address (auto-detected from headers if omitted)
    • headers: Request headers for IP detection (Pages Router)
    • action: Reject tokens whose action field doesn't match
    • hostname: Reject tokens whose hostname field doesn't match
    • timeout: Fetch timeout in milliseconds (default: 10000)

Returns: Promise<boolean>true when the token is valid

Throws: TurnstileError — when verification fails (inspect .errorCodes)

Error codes — in addition to Cloudflare's error codes, the following are added by this library:

| Code | Meaning | |------|---------| | timeout-error | The siteverify request timed out | | action-mismatch | Response action didn't match the expected value | | hostname-mismatch | Response hostname didn't match the expected value |

Utility Functions

These client-side utilities are SSR-safe and can be imported anywhere:

import {
  // Script loading
  loadTurnstileScript,
  isTurnstileLoaded,

  // Widget control
  resetTurnstile,
  removeTurnstile,
  getTurnstileResponse,
  executeTurnstile,
  isTokenExpired,
  renderTurnstile,
} from "nextjs-turnstile";

| Function | Description | |----------|-------------| | loadTurnstileScript() | Load the Turnstile script (returns Promise) | | isTurnstileLoaded() | Check if script is loaded | | resetTurnstile(widgetRef?) | Reset a widget | | removeTurnstile(widgetRef) | Remove a widget from the page | | getTurnstileResponse(widgetRef) | Get token from a widget | | executeTurnstile(widgetRef) | Execute challenge on a widget | | isTokenExpired(widgetRef) | Check if token is expired | | renderTurnstile(container, options) | Render a widget (low-level API) |

Size Options

| Size | Dimensions | Use Case | |------|------------|----------| | normal | 300×65px | Standard forms | | compact | 150×140px | Space-constrained layouts | | flexible | 100% width (min 300px), 65px | Responsive designs |

Examples

Deferred Execution

Only run the challenge when the user clicks submit:

function DeferredForm() {
  const turnstileRef = useRef<TurnstileRef>(null);
  const [token, setToken] = useState<string | null>(null);

  const handleSubmit = async () => {
    // Start the challenge
    turnstileRef.current?.execute();
    
    // Wait for token via onSuccess callback
    // The form will submit once token is set
  };

  useEffect(() => {
    if (token) {
      // Token received, submit the form
      submitForm(token);
    }
  }, [token]);

  return (
    <form>
      <Turnstile
        ref={turnstileRef}
        execution="execute"
        appearance="interaction-only"
        onSuccess={setToken}
      />
      <button type="button" onClick={handleSubmit}>
        Submit
      </button>
    </form>
  );
}

Invisible Mode

When your Turnstile widget type is set to Invisible in the Cloudflare Dashboard, the challenge runs entirely in the background with zero visual footprint. No widget is ever shown to the user.

Note: The widget type (Managed / Non-Interactive / Invisible) is configured in the Cloudflare Dashboard when creating your sitekey. The appearance and size props are ignored for invisible widgets.

Auto-run (recommended for most cases)

The challenge runs automatically when the component mounts. The token is passed to onSuccess as soon as it's available:

function InvisibleAutoForm() {
  const [token, setToken] = useState<string | null>(null);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!token) return;

    await fetch("/api/submit", {
      method: "POST",
      headers: { "cf-turnstile-response": token },
      body: JSON.stringify({ /* form data */ }),
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Your form fields */}
      <Turnstile
        onSuccess={setToken}
        onExpire={() => setToken(null)}
        feedbackEnabled={false}
      />
      <button type="submit" disabled={!token}>Submit</button>
    </form>
  );
}

Deferred execution

Run the challenge only when the user submits, using execution="execute":

function InvisibleDeferredForm() {
  const turnstileRef = useRef<TurnstileRef>(null);
  const [token, setToken] = useState<string | null>(null);

  const handleSubmit = async () => {
    if (!turnstileRef.current?.isReady()) return;
    turnstileRef.current.execute();
  };

  useEffect(() => {
    if (token) {
      // Token received — submit form
      fetch("/api/submit", {
        method: "POST",
        headers: { "cf-turnstile-response": token },
        body: JSON.stringify({ /* form data */ }),
      });
    }
  }, [token]);

  return (
    <form>
      {/* Your form fields */}
      <Turnstile
        ref={turnstileRef}
        execution="execute"
        onSuccess={setToken}
        feedbackEnabled={false}
      />
      <button type="button" onClick={handleSubmit}>Submit</button>
    </form>
  );
}

Protecting API endpoints (Troy Hunt pattern)

Use an invisible widget to protect API calls by attaching the token as a header:

function ProtectedSearch() {
  const [token, setToken] = useState<string | null>(null);
  const turnstileRef = useRef<TurnstileRef>(null);

  const handleSearch = async (query: string) => {
    if (!token) return;

    const res = await fetch(`/api/search?q=${query}`, {
      headers: { "cf-turnstile-response": token },
    });

    // Reset the widget for the next request (tokens are single-use)
    turnstileRef.current?.reset();
    setToken(null);

    return res.json();
  };

  return (
    <div>
      <Turnstile
        ref={turnstileRef}
        onSuccess={setToken}
        onExpire={() => setToken(null)}
        feedbackEnabled={false}
      />
      <SearchInput onSearch={handleSearch} disabled={!token} />
    </div>
  );
}

With React Hook Form

import { useForm } from "react-hook-form";
import { Turnstile } from "nextjs-turnstile";

function HookFormExample() {
  const { register, handleSubmit, setValue, formState } = useForm();

  const onSubmit = async (data) => {
    const response = await fetch("/api/submit", {
      method: "POST",
      body: JSON.stringify(data),
    });
    // Handle response...
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email")} />
      
      <Turnstile
        onSuccess={(token) => setValue("turnstileToken", token)}
        onExpire={() => setValue("turnstileToken", "")}
      />
      <input type="hidden" {...register("turnstileToken", { required: true })} />
      
      <button type="submit" disabled={!formState.isValid}>
        Submit
      </button>
    </form>
  );
}

Multiple Widgets

Each widget needs a unique key when using multiple on the same page:

function MultipleWidgets() {
  return (
    <div>
      <Turnstile
        key="widget-1"
        responseFieldName="captcha-1"
        onSuccess={(token) => console.log("Widget 1:", token)}
      />
      <Turnstile
        key="widget-2"
        responseFieldName="captcha-2"
        onSuccess={(token) => console.log("Widget 2:", token)}
      />
    </div>
  );
}

Migration from v0.x

Version 1.0.0 is a breaking change with a simplified API:

// Before (v0.x)
import { TurnstileImplicit, TurnstileExplicit } from "nextjs-turnstile";

<TurnstileImplicit
  responseFieldName="my-token"
  onSuccess={handleSuccess}
/>

// After (v1.0.0)
import { Turnstile } from "nextjs-turnstile";

<Turnstile
  responseFieldName="my-token"
  onSuccess={handleSuccess}
/>

Key changes:

  • Single Turnstile component replaces both TurnstileImplicit and TurnstileExplicit
  • Uses explicit rendering internally for better React compatibility
  • Added imperative API via ref
  • Added new props: execution, retry, retryInterval, action, cData, onLoad, etc.
  • Requires React 18+ and Next.js 13+

Troubleshooting

Widget not appearing

  1. Check that your site key is correct
  2. Ensure you're running on http:// or https:// (not file://)
  3. Check the browser console for errors

Token verification fails

  1. Verify your secret key is correct
  2. Tokens expire after 5 minutes - ensure quick submission
  3. Each token can only be verified once

Widget resets unexpectedly

This usually happens when React re-renders the component. Ensure:

  1. The siteKey prop is stable (not recreated each render)
  2. Parent components don't unmount/remount the Turnstile component
  3. Use key prop if you need to force a reset

Resources

License

MIT © Davod Mozafari