multiple-widgets
Handle multiple Turnstile widgets on the same page without conflicts. Activate when adding more than one CAPTCHA to a page, or when widgets interfere with each other.
Multiple Widgets
Handle multiple Turnstile widgets on the same page without conflicts or race conditions.
Basic Multiple Widgets
When using multiple widgets, always provide unique IDs:
import { Turnstile } from '@marsidev/react-turnstile'
export default function MultiFormPage() {
return (
<>
<section>
<h2>Newsletter Signup</h2>
<form>
<input type="email" placeholder="Email" />
<Turnstile
id="newsletter-widget"
siteKey="YOUR_SITE_KEY"
/>
<button type="submit">Subscribe</button>
</form>
</section>
<section>
<h2>Contact Form</h2>
<form>
<input type="email" placeholder="Email" />
<textarea placeholder="Message" />
<Turnstile
id="contact-widget"
siteKey="YOUR_SITE_KEY"
/>
<button type="submit">Send</button>
</form>
</section>
</>
)
}
With Separate Refs
Control each widget independently:
import { Turnstile } from '@marsidev/react-turnstile'
import type { TurnstileInstance } from '@marsidev/react-turnstile'
import { useRef } from 'react'
export default function MultiFormPage() {
const newsletterRef = useRef<TurnstileInstance>(null)
const contactRef = useRef<TurnstileInstance>(null)
async function handleNewsletterSubmit(e: React.FormEvent) {
e.preventDefault()
const token = newsletterRef.current?.getResponse()
if (!token) {
alert('Please complete the CAPTCHA')
return
}
// Submit form...
// Reset only this widget
newsletterRef.current?.reset()
}
async function handleContactSubmit(e: React.FormEvent) {
e.preventDefault()
const token = contactRef.current?.getResponse()
if (!token) {
alert('Please complete the CAPTCHA')
return
}
// Submit form...
// Reset only this widget
contactRef.current?.reset()
}
return (
<>
<section>
<h2>Newsletter Signup</h2>
<form onSubmit={handleNewsletterSubmit}>
<input type="email" placeholder="Email" />
<Turnstile
ref={newsletterRef}
id="newsletter-widget"
siteKey="YOUR_SITE_KEY"
/>
<button type="submit">Subscribe</button>
</form>
</section>
<section>
<h2>Contact Form</h2>
<form onSubmit={handleContactSubmit}>
<input type="email" placeholder="Email" />
<textarea placeholder="Message" />
<Turnstile
ref={contactRef}
id="contact-widget"
siteKey="YOUR_SITE_KEY"
/>
<button type="submit">Send</button>
</form>
</section>
</>
)
}
Manual Script Injection (Recommended for Multiple Widgets)
When using multiple widgets, manual script injection ensures optimal loading:
import {
Turnstile,
SCRIPT_URL,
DEFAULT_SCRIPT_ID
} from '@marsidev/react-turnstile'
import Script from 'next/script'
export default function MultiFormPage() {
return (
<>
{/* Single script for all widgets */}
<Script
id={DEFAULT_SCRIPT_ID}
src={SCRIPT_URL}
strategy="afterInteractive"
/>
<section>
<h2>Form 1</h2>
<Turnstile
id="widget-1"
siteKey="YOUR_SITE_KEY"
injectScript={false}
/>
</section>
<section>
<h2>Form 2</h2>
<Turnstile
id="widget-2"
siteKey="YOUR_SITE_KEY"
injectScript={false}
/>
</section>
</>
)
}
Dynamic Widgets (Conditional Rendering)
When widgets appear/disappear based on state:
import { Turnstile } from '@marsidev/react-turnstile'
import type { TurnstileInstance } from '@marsidev/react-turnstile'
import { useRef, useState } from 'react'
export default function DynamicForms() {
const [showNewsletter, setShowNewsletter] = useState(false)
const [showContact, setShowContact] = useState(false)
const newsletterRef = useRef<TurnstileInstance>(null)
const contactRef = useRef<TurnstileInstance>(null)
return (
<>
<button onClick={() => setShowNewsletter(!showNewsletter)}>
Toggle Newsletter
</button>
<button onClick={() => setShowContact(!showContact)}>
Toggle Contact
</button>
{showNewsletter && (
<section>
<h2>Newsletter</h2>
<Turnstile
ref={newsletterRef}
id="newsletter-widget"
siteKey="YOUR_SITE_KEY"
/>
</section>
)}
{showContact && (
<section>
<h2>Contact</h2>
<Turnstile
ref={contactRef}
id="contact-widget"
siteKey="YOUR_SITE_KEY"
/>
</section>
)}
</>
)
}
Tabbed Interface
Multiple forms in tabs:
import { Turnstile } from '@marsidev/react-turnstile'
import type { TurnstileInstance } from '@marsidev/react-turnstile'
import { useRef, useState } from 'react'
export default function TabbedForms() {
const [activeTab, setActiveTab] = useState<'login' | 'signup'>('login')
const loginRef = useRef<TurnstileInstance>(null)
const signupRef = useRef<TurnstileInstance>(null)
return (
<>
<div role="tablist">
<button onClick={() => setActiveTab('login')}>Login</button>
<button onClick={() => setActiveTab('signup')}>Sign Up</button>
</div>
{activeTab === 'login' && (
<form>
<input type="email" placeholder="Email" />
<input type="password" placeholder="Password" />
<Turnstile
ref={loginRef}
id="login-widget"
siteKey="YOUR_SITE_KEY"
/>
<button type="submit">Login</button>
</form>
)}
{activeTab === 'signup' && (
<form>
<input type="email" placeholder="Email" />
<input type="password" placeholder="Password" />
<input type="password" placeholder="Confirm Password" />
<Turnstile
ref={signupRef}
id="signup-widget"
siteKey="YOUR_SITE_KEY"
/>
<button type="submit">Sign Up</button>
</form>
)}
</>
)
}
Common Mistakes
❌ Using Same ID for Multiple Widgets
Wrong:
<>
<Turnstile siteKey="xxx" /> // Uses default "cf-turnstile" id
<Turnstile siteKey="xxx" /> // Also uses "cf-turnstile" id - CONFLICT!
</>
Result: Widgets interfere with each other, only one works correctly.
Correct:
<>
<Turnstile id="widget-1" siteKey="xxx" />
<Turnstile id="widget-2" siteKey="xxx" />
</>
❌ Script Loading Race Conditions
Wrong:
// Each widget tries to inject the script independently
<Turnstile siteKey="xxx" />
<Turnstile siteKey="xxx" />
Result: Race conditions, widgets may fail to initialize.
Correct:
// Single script injection for all widgets
<Script id={DEFAULT_SCRIPT_ID} src={SCRIPT_URL} />
<Turnstile id="widget-1" siteKey="xxx" injectScript={false} />
<Turnstile id="widget-2" siteKey="xxx" injectScript={false} />
❌ Sharing Refs Between Widgets
Wrong:
const ref = useRef<TurnstileInstance>(null)
<>
<Turnstile ref={ref} id="widget-1" siteKey="xxx" />
<Turnstile ref={ref} id="widget-2" siteKey="xxx" /> // ❌ Same ref!
</>
Result: Both widgets share state, methods don't work correctly.
Correct:
const ref1 = useRef<TurnstileInstance>(null)
const ref2 = useRef<TurnstileInstance>(null)
<>
<Turnstile ref={ref1} id="widget-1" siteKey="xxx" />
<Turnstile ref={ref2} id="widget-2" siteKey="xxx" />
</>
Debugging Multiple Widgets
If widgets aren't working correctly:
- Check unique IDs: Ensure all widgets have different id props
- Check console: Look for "Turnstile has not been loaded" warnings
- Inspect DOM: Verify containers have different IDs
- Script loading: Use manual injection with injectScript={false}
Performance Considerations
- Each widget makes a request to Cloudflare
- Consider if you really need multiple widgets vs. one widget protecting multiple actions
- Manual script injection prevents duplicate script loading
See Also
- basic-setup skill - Single widget setup
- token-lifecycle skill - Handling tokens with multiple widgets
- nextjs-ssr skill - Next.js integration with multiple widgets