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

@tbmwebui/nextjs-turnstile

v1.1.1

Published

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

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 }}
  
  // 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 API.

import { verifyTurnstile } from "nextjs-turnstile";

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

// With options
const isValid = await verifyTurnstile(token, {
  secretKey: "custom-secret-key",  // Override secret key
  ip: "1.2.3.4",                   // User's IP (auto-detected if not provided)
  headers: request.headers,        // For IP detection in Pages Router
});

Parameters:

  • token (string): The token from the Turnstile widget
  • options (object, optional):
    • secretKey: Override the default secret key
    • ip: User's IP address (auto-detected from headers)
    • headers: Request headers for IP detection

Returns: Promise<boolean> - true if valid, false otherwise

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

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