Embed the widget
Learn how to add the Turnstile widget to your webpage using implicit or explicit rendering methods.
Before you begin, you must have:
- A Cloudflare account
- A Turnstile widget with a sitekey
- Access to edit your website's HTML
- Basic knowledge of HTML and JavaScript
- Page load: The Turnstile script loads and scans for elements or waits for programmatic calls.
- Widget rendering: Widgets are created and begin running challenges.
- Token generation: When a challenge is completed, a token is generated.
- Form integration: The token is made available via callbacks or hidden form fields.
- Server validation: Your server receives the token and validates it using the Siteverify API.
Implicit rendering automatically scans your HTML for elements with the cf-turnstile
class and renders widgets when the page loads.
Cloudflare recommends using implicit rendering on the following scenarios:
- You have simple implementations and want a quick integration.
- You have static websites with straightforward forms.
- You want widgets to appear immediately on pageload.
- You do not need programmatic control of the widget.
Add the Turnstile JavaScript API to your HTML.
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
Add widget containers where you want the challenges to appear on your website.
<div class="cf-turnstile" data-sitekey="<YOUR-SITE-KEY>"></div>
Customize your widgets using data attributes.
<div class="cf-turnstile" data-sitekey="<YOUR-SITE-KEY>" data-theme="light" data-size="normal" data-callback="onSuccess"></div>
Basic login form
<!DOCTYPE html><html><head> <title>Login Form</title> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script></head><body> <form action="/login" method="POST"> <input type="text" name="username" placeholder="Username" required /> <input type="password" name="password" placeholder="Password" required />
<!-- Turnstile widget with basic configuration --> <div class="cf-turnstile" data-sitekey="<YOUR-SITE-KEY>"></div> <button type="submit">Log in</button> </form></body></html>
Advanced form with callbacks
<form action="/contact" method="POST" id="contact-form"> <input type="email" name="email" placeholder="Email" required /> <textarea name="message" placeholder="Message" required></textarea><!-- Widget with callbacks and custom configuration --> <div class="cf-turnstile" data-sitekey="<YOUR-SITE-KEY>" data-theme="auto" data-size="flexible" data-callback="onTurnstileSuccess" data-error-callback="onTurnstileError" data-expired-callback="onTurnstileExpired"></div> <button type="submit" id="submit-btn" disabled>Send Message</button></form>
<script>function onTurnstileSuccess(token) { console.log('Turnstile success:', token); document.getElementById('submit-btn').disabled = false;}function onTurnstileError(errorCode) { console.error('Turnstile error:', errorCode); document.getElementById('submit-btn').disabled = true;}function onTurnstileExpired() { console.warn('Turnstile token expired'); document.getElementById('submit-btn').disabled = true;}</script>
Multiple widgets with different configurations
<!-- Compact widget for newsletter signup --><form action="/newsletter" method="POST"> <input type="email" name="email" placeholder="Email" /> <div class="cf-turnstile" data-sitekey="<YOUR-SITE-KEY>" data-size="compact" data-action="newsletter"></div> <button type="submit">Subscribe</button></form>
<!-- Normal widget for contact form --><form action="/contact" method="POST"> <input type="text" name="name" placeholder="Name" /> <input type="email" name="email" placeholder="Email" /> <textarea name="message" placeholder="Message"></textarea> <div class="cf-turnstile" data-sitekey="<YOUR-SITE-KEY>" data-action="contact" data-theme="dark"></div> <button type="submit">Send</button></form>
Automatic form integration
When you embed a Turnstile widget inside a <form>
element, an invisible input field with the name cf-turnstile-response
is automatically created. This field contains the verification token and gets submitted with your other form data.
<form action="/submit" method="POST"> <input type="text" name="data" /> <div class="cf-turnstile" data-sitekey="<YOUR-SITE-KEY>"></div> <!-- Hidden field automatically added: --> <!-- <input type="hidden" name="cf-turnstile-response" value="TOKEN_VALUE" /> --> <button type="submit">Submit</button></form>
Explicit rendering gives you programmatic control over when and how widgets are created using JavaScript functions.
Cloudflare recommends using explicit rendering on the following scenarios:
- You have dynamic websites and single-page applications (SPAs).
- You need to control the timing of widget creation.
- You want to conditionally render the widget based on visitor interactions.
- You want multiple widgets with different configurations.
- You have complex applications requiring widget lifecycle management.
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" defer></script>
Create containers without the cf-turnstile
class.
<div id="turnstile-container"></div>
Call turnstile.render()
when you are ready to create the widget.
const widgetId = turnstile.render('#turnstile-container', { sitekey: '<YOUR-SITE-KEY>', callback: function(token) { console.log('Success:', token); }});
Basic explicit implementation
<!DOCTYPE html><html><head> <title>Explicit Rendering</title> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" defer></script></head><body> <form id="login-form"> <input type="text" name="username" placeholder="Username" /> <input type="password" name="password" placeholder="Password" /> <div id="turnstile-widget"></div> <button type="submit">Login</button> </form>
<script> window.onload = function() { turnstile.render('#turnstile-widget', { sitekey: '<YOUR-SITE-KEY>', callback: function(token) { console.log('Turnstile token:', token); // Handle successful verification }, 'error-callback': function(errorCode) { console.error('Turnstile error:', errorCode); } }); }; </script></body></html>
Using onload callback
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onTurnstileLoad" defer></script><div id="widget-container"></div><script>function onTurnstileLoad() { turnstile.render('#widget-container', { sitekey: '<YOUR-SITE-KEY>', theme: 'light', callback: function(token) { console.log('Challenge completed:', token); } });}</script>
Advanced SPA implementation
<div id="dynamic-form-container"></div>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
<script>class TurnstileManager { constructor() { this.widgets = new Map(); } createWidget(containerId, config) { // Wait for Turnstile to be ready turnstile.ready(() => { const widgetId = turnstile.render(containerId, { sitekey: config.sitekey, theme: config.theme || 'auto', size: config.size || 'normal', callback: (token) => { console.log(`Widget ${widgetId} completed:`, token); if (config.onSuccess) config.onSuccess(token, widgetId); }, 'error-callback': (error) => { console.error(`Widget ${widgetId} error:`, error); if (config.onError) config.onError(error, widgetId); } });
this.widgets.set(containerId, widgetId); return widgetId; }); } removeWidget(containerId) { const widgetId = this.widgets.get(containerId); if (widgetId) { turnstile.remove(widgetId); this.widgets.delete(containerId); } } resetWidget(containerId) { const widgetId = this.widgets.get(containerId); if (widgetId) { turnstile.reset(widgetId); } }}
// Usageconst manager = new TurnstileManager();
// Create a widget when user clicks a buttondocument.getElementById('show-form-btn').addEventListener('click', () => { document.getElementById('dynamic-form-container').innerHTML = ` <form> <input type="email" placeholder="Email" /> <div id="turnstile-widget"></div> <button type="submit">Submit</button> </form> `; manager.createWidget('#turnstile-widget', { sitekey: '<YOUR-SITE-KEY>', theme: 'dark', onSuccess: (token) => { // Handle successful verification console.log('Form ready for submission'); } });});</script>
Explicit rendering provides full control over the widget lifecycle.
// Render a widgetconst widgetId = turnstile.render('#container', { sitekey: '<YOUR-SITE-KEY>', callback: handleSuccess});
// Get the current tokenconst token = turnstile.getResponse(widgetId);
// Check if widget is expiredconst isExpired = turnstile.isExpired(widgetId);
// Reset the widget (clears current state)turnstile.reset(widgetId);
// Remove the widget completelyturnstile.remove(widgetId);
Control when challenges run with execution modes.
// Render widget but don't run challenge yetconst widgetId = turnstile.render('#container', { sitekey: '<YOUR-SITE-KEY>', execution: 'execute' // Don't auto-execute});
// Later, run the challenge when neededturnstile.execute('#container');
Both implicit and explicit rendering methods support the same configuration options. Refer to the table below for the most commonly used configurations.
Option | Description | Values |
---|---|---|
sitekey | Your widget's sitekey | Required string |
theme | Visual theme | auto , light , dark |
size | Widget size | normal , flexible , compact |
callback | Success callback | Function |
error-callback | Error callback | Function |
execution | When to run the challenge | render , execute |
appearance | When the widget is visible | always , execute , interaction-only |
For a complete list of configuration options, refer to Widget configurations.
You can test your Turnstile widget on your webpage without triggering an actual Cloudflare Challenge by using a testing sitekey.
Refer to Testing for more information.
-
Server-side validation is mandatory. It is critical to enforce Turnstile tokens with the Siteverify API. The Turnstile token could be invalid, expired, or already redeemed. Not verifying the token will leave major vulnerabilities in your implementation. You must call Siteverify to complete your Turnstile configuration. Otherwise, it is incomplete and will result in zeroes for token validation when viewing your metrics in Turnstile Analytics.
-
Tokens expire after 300 seconds (5 minutes). Each token can only be validated once. Expired or used tokens must be replaced with fresh challenges.
Was this helpful?
- Resources
- API
- New to Cloudflare?
- Directory
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- © 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark