Cloudflare Turnstile Examples
Cloudflare Turnstile provides seamless bot protection with minimal user friction. Widget behavior is configured in the Cloudflare dashboard, offering flexible appearance and execution modes.
Visible Turnstile
Visible Turnstile displays a widget that users interact with. By default, it executes automatically on render.
Key Points for Turnstile
Configuration
- Widget behavior: Configured in Cloudflare dashboard, not just code
- Appearance modes:
always: Widget always visibleinteraction-only: Shows only when user interaction detectedexecute: Shows only when manually executed
- Execution modes:
render: Executes immediately when rendered (default)execute: Waits for manual execution viaexecute()method
- Size options:
normal,compact,flexible
Best Practices
- Use
execution: 'execute'withappearance: 'execute'for invisible-like behavior - Configure challenge difficulty and appearance in Cloudflare dashboard
- Always handle all three event callbacks (
onToken,onError,onExpired) - Reset widgets after successful form submission
Basic Implementation
import { Captcha } from '@thg-altitude/captcha';
const turnstile = new Captcha({
provider: 'turnstile',
siteKey: 'your-turnstile-site-key'
});
await turnstile.render('captcha-container', 'contact-form', {
size: 'normal', // 'normal' | 'compact' | 'flexible'
appearance: 'always', // 'always' | 'interaction-only' | 'execute'
onToken: (captchaToken) => {
console.log('Turnstile solved:', captchaToken);
setToken(captchaToken);
},
onError: () => {
showErrorMessage('Security verification failed. Please try again.');
},
onExpired: () => {
console.log('Turnstile token expired');
showMessage('Security verification expired. Please complete it again.');
}
});
React Implementation
import { useEffect, useRef, useState } from 'react';
import { Captcha } from '@thg-altitude/captcha';
function ContactForm() {
const captchaRef = useRef<HTMLDivElement>(null);
const captcha = useRef<Captcha>();
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const [error, setError] = useState<string>('');
useEffect(() => {
captcha.current = new Captcha({
provider: 'turnstile',
siteKey: 'your-turnstile-site-key'
});
if (captchaRef.current) {
captcha.current.render(captchaRef.current, 'contact-form', {
size: 'normal',
appearance: 'always',
onToken: handleCaptchaToken,
onError: handleCaptchaError,
onExpired: handleCaptchaExpired
});
}
return () => {
captcha.current?.reset('contact-form');
};
}, []);
const handleCaptchaToken = (token: string) => {
setCaptchaToken(token);
setError('');
};
const handleCaptchaError = () => {
setCaptchaToken(null);
setError('Security verification failed. Please try again.');
};
const handleCaptchaExpired = () => {
setCaptchaToken(null);
setError('Security verification expired. Please complete it again.');
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!captchaToken) {
setError('Please complete the security verification.');
return;
}
try {
await submitForm(captchaToken);
captcha.current?.reset('contact-form');
setCaptchaToken(null);
} catch (error) {
setError('Form submission failed. Please try again.');
}
};
return (
<form onSubmit={handleSubmit}>
<input type="email" placeholder="Email" required />
<textarea placeholder="Message" required />
{error && <div className="error-message">{error}</div>}
<div ref={captchaRef} id="captcha-container"></div>
<button type="submit" disabled={!captchaToken}>
Submit
</button>
</form>
);
}
Vue Implementation
<template>
<form @submit.prevent="handleSubmit">
<input v-model="email" type="email" placeholder="Email" required />
<textarea v-model="message" placeholder="Message" required />
<div v-if="error" class="error-message">{{ error }}</div>
<div ref="captchaContainer" id="captcha-container"></div>
<button type="submit" :disabled="!captchaToken">Submit</button>
</form>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { Captcha } from '@thg-altitude/captcha';
const captchaContainer = ref<HTMLDivElement>();
const captchaToken = ref<string | null>(null);
const error = ref<string>('');
const email = ref('');
const message = ref('');
let captcha: Captcha;
onMounted(async () => {
captcha = new Captcha({
provider: 'turnstile',
siteKey: 'your-turnstile-site-key'
});
if (captchaContainer.value) {
await captcha.render(captchaContainer.value, 'vue-form', {
size: 'normal',
appearance: 'always',
onToken: handleCaptchaToken,
onError: handleCaptchaError,
onExpired: handleCaptchaExpired
});
}
});
onUnmounted(() => {
captcha?.reset('vue-form');
});
const handleCaptchaToken = (token: string) => {
captchaToken.value = token;
error.value = '';
};
const handleCaptchaError = () => {
captchaToken.value = null;
error.value = 'Security verification failed. Please try again.';
};
const handleCaptchaExpired = () => {
captchaToken.value = null;
error.value = 'Security verification expired. Please complete it again.';
};
const handleSubmit = async () => {
if (!captchaToken.value) {
error.value = 'Please complete the security verification.';
return;
}
try {
await submitForm(captchaToken.value);
// Reset form
email.value = '';
message.value = '';
captcha.reset('vue-form');
captchaToken.value = null;
} catch (err) {
error.value = 'Form submission failed. Please try again.';
}
};
</script>
Invisible/Execute Mode Turnstile
Turnstile can be configured to defer execution until manually triggered, similar to invisible reCAPTCHA/hCaptcha.
Basic Implementation
import { Captcha } from '@thg-altitude/captcha';
const turnstile = new Captcha({
provider: 'turnstile',
siteKey: 'your-turnstile-site-key'
});
await turnstile.render('container-id', 'form-protection', {
appearance: 'execute', // Widget appears only when executed
execution: 'execute', // Defer execution until execute() is called
onToken: (token) => {
// Automatically submit form when token is received
submitForm(token);
},
onError: () => {
console.error('Turnstile verification failed');
}
});
// Execute on form submission
document.getElementById('submit-btn').onclick = async () => {
try {
await turnstile.execute('form-protection');
// Token will be provided via onToken callback
} catch (error) {
console.error('CAPTCHA verification failed:', error);
}
};
React Implementation
import { useEffect, useState } from 'react';
import { Captcha } from '@thg-altitude/captcha';
function ContactForm() {
let turnstile;
const [form, setForm] = useState({ name: '', email: '', message: '' });
const captchaId = 'contact-form';
const containerId = 'turnstile-container';
useEffect(() => {
turnstile = new Captcha({
provider: 'turnstile',
siteKey: 'your-turnstile-site-key'
});
turnstile.render(containerId, captchaId, {
appearance: 'execute',
execution: 'execute',
onToken: (token) => {
// Token received, submit form automatically
submitForm(token);
},
onError: () => {
console.error('Turnstile verification failed');
}
});
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
try {
// Trigger CAPTCHA verification before form submission
await turnstile.execute(captchaId);
// Form submission happens in onToken callback
} catch (error) {
console.error('Turnstile failed:', error);
}
};
const submitForm = async (captchaToken) => {
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Captcha-Response': captchaToken
},
body: JSON.stringify(form)
});
if (response.ok) {
// Reset form and captcha
setForm({ name: '', email: '', message: '' });
turnstile.reset(captchaId);
}
} catch (error) {
console.error('Form submission failed:', error);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Name"
value={form.name}
onChange={(e) => setForm({...form, name: e.target.value})}
required
/>
<input
type="email"
placeholder="Email"
value={form.email}
onChange={(e) => setForm({...form, email: e.target.value})}
required
/>
<textarea
placeholder="Message"
value={form.message}
onChange={(e) => setForm({...form, message: e.target.value})}
required
/>
<div id={containerId}></div>
<button type="submit">Submit</button>
</form>
);
}
Vue Implementation
<template>
<form @submit.prevent="handleSubmit">
<input v-model="form.name" type="text" placeholder="Name" required />
<input v-model="form.email" type="email" placeholder="Email" required />
<textarea v-model="form.message" placeholder="Message" required></textarea>
<div id="captchaContainer"></div>
<button type="submit">Submit</button>
</form>
</template>
<script setup>
import { ref, onMounted, onUnmounted, reactive } from 'vue';
import { Captcha } from '@thg-altitude/captcha';
let captcha: Captcha;
const captchaId = 'contact-form';
const containerId = 'captchaContainer';
const form = reactive({
name: '',
email: '',
message: ''
});
onMounted(async () => {
captcha = new Captcha({
provider: 'turnstile',
siteKey: 'your-turnstile-site-key'
});
await captcha.render(containerId, captchaId, {
appearance: 'execute',
execution: 'execute',
onToken: (captchaToken) => {
submitForm(captchaToken);
},
onError: () => {
console.error('Error rendering captcha:', captchaId);
}
});
});
onUnmounted(() => {
captcha?.reset(captchaId);
});
const submitForm = async (token) => {
try {
await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Captcha-Response': token
},
body: JSON.stringify(form)
});
// Reset form
Object.assign(form, { name: '', email: '', message: '' });
captcha.reset(captchaId);
} catch (error) {
console.error('Form submission failed:', error);
}
};
const handleSubmit = async () => {
if (!captcha) return;
try {
await captcha.execute(captchaId);
} catch (error) {
console.error('Turnstile failed:', error);
}
};
</script>