React Turnstile automatically injects the Cloudflare Turnstile script into your page. This page explains how script injection works, how to customize it, and how to handle it manually for advanced use cases.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.
Default Behavior
By default, the Turnstile component automatically injects the Cloudflare script when it mounts:- Injects the script:
https://challenges.cloudflare.com/turnstile/v0/api.js - Waits for it to load
- Renders the widget
Single Script Instance: The script is injected only once per page, even if you have multiple Turnstile widgets. All widgets share the same global
window.turnstile instance.How Script Injection Works
The injection logic is defined inutils.ts:22-71:
Key Features
- Duplicate Prevention: Checks for existing script by ID and src before injection
- Onload Callback: Uses query parameter
?onload=for load notification - Explicit Render: Always uses
render=explicitto control rendering via React - Async/Defer: Scripts are async and deferred by default for better performance
Customizing Script Options
Use thescriptOptions prop to customize script injection:
ScriptOptions Interface
Custom nonce for the injected script. Required for Content Security Policy (CSP) compliance.
Whether to set the injected script as defer.
Whether to set the injected script as async.
Where to inject the script in the DOM.
Custom ID for the injected script element.
Custom name for the global onload callback function.
Callback invoked when the script fails to load (e.g., Cloudflare has an outage).
Custom crossOrigin attribute for the injected script.
Content Security Policy (CSP)
When using Content Security Policy, you need to allow the Turnstile script and provide a nonce.CSP Headers
Add these directives to your CSP policy:Using Nonce
Generate a nonce on your server and pass it to the component:Manual Script Injection
For advanced use cases, you can disable automatic injection and load the script yourself.Disable Automatic Injection
Manual Injection Methods
Method 1: HTML Script Tag
Method 1: HTML Script Tag
Add the script directly in your HTML:Then in React:
Method 2: Next.js Script Component
Method 2: Next.js Script Component
Use Next.js
<Script> component:Method 3: Custom Loader Hook
Method 3: Custom Loader Hook
Create a custom script loader:
Method 4: Import in _app or Layout
Method 4: Import in _app or Layout
Load once for all pages:Next.js Pages Router (_app.tsx):Next.js App Router (layout.tsx):Then use
injectScript={false} on all widgets:Script Load Callbacks
React when the script finishes loading:onLoadScript Callback
Script Error Handling
Script Loading States
The library tracks script loading internally using a state machine:lib.tsx:22-46
States
- unloaded: Script hasn’t been injected yet
- loading: Script is currently loading
- ready: Script loaded,
window.turnstileis available
This state is shared globally across all Turnstile instances on the page.
Multiple Widgets
When you have multiple Turnstile widgets on the same page:- First widget injects the script (if not already present)
- All widgets wait for
turnstileState === 'ready' - Each widget renders independently once the script is loaded
- Only one script tag exists in the DOM
Troubleshooting
Script Not Loading
Script Not Loading
Symptoms: Widget never appears, console shows no errorsSolutions:
- Check if the script is being blocked by ad blockers or privacy extensions
- Verify CSP headers allow
https://challenges.cloudflare.com - Check network tab for failed script requests
- Add
onLoadScriptandscriptOptions.onErrorto debug:
CSP Violations
CSP Violations
Symptoms: Console errors like “Refused to load script” or “Refused to frame”Solutions:
- Add Cloudflare domains to CSP:
- Pass nonce to component:
Duplicate Scripts
Duplicate Scripts
Symptoms: Multiple script tags with same src in DOMSolutions:
This shouldn’t happen due to built-in duplicate prevention, but if it does:
- Use
injectScript={false}and inject manually once - Check for conflicting script injection libraries
- Ensure all Turnstile instances use the same
scriptOptions.id
Manual Injection Not Working
Manual Injection Not Working
Symptoms: Widget doesn’t render with
injectScript={false}Solutions:- Ensure script URL includes
?render=explicit:
- Wait for script to load before rendering widget:
Best Practices
Use Auto-Injection
Let the component handle script injection unless you have a specific reason not to
Include Nonce for CSP
Always provide a nonce when using Content Security Policy
Handle Load Errors
Implement
scriptOptions.onError to gracefully handle script load failuresOne Script Per Page
Don’t manually inject the script if you’re also using auto-injection
Next Steps
Component Props
Explore all component configuration options
Widget Lifecycle
Understand widget lifecycle and callbacks
Script Options API
View all script configuration options
Troubleshooting
Handle script and widget errors