@delaneydev/laravel-turnstile-vue

A production-ready, SSR-safe Cloudflare Turnstile component for Vue 3.
1,937 9
Install
npm install @delaneydev/laravel-turnstile-vue
Latest Version:2.0.0
Vue:3
License:MIT
Last Updated:Oct 1, 2026
Links: GitHub  ·  npm
Maintainer: delaneydev

Laravel Turnstile Vue

Delaney Industries Ltd

Free and open source software from Delaney Industries Ltd.

npm version npm downloads CI license

A typed, production-ready Cloudflare Turnstile component for Vue 3. It is safe to import during SSR, supports visible and invisible widgets, and works with any backend that validates Turnstile tokens.

The npm name is retained for existing users, but Laravel and Inertia are optional.

Highlights

  • Stable SSR markup with no browser globals accessed during import or server render
  • Shared, race-safe script loader with CSP nonce and consumer-managed script support
  • Every current Cloudflare render option and lifecycle callback
  • Visible, deferred, and invisible challenge flows
  • Typed v-model, events, errors, methods, props, and window.turnstile
  • Single-flight challenge() calls and stale-token protection
  • Optional async preflight before a challenge, useful for refreshing long-lived sessions
  • Automatic cleanup across Vue and Inertia navigation
  • ESM and CommonJS artifacts with bundled declarations
  • Official Cloudflare test keys exported for deterministic integration tests

Install

npm install @delaneydev/laravel-turnstile-vue

Vue is a peer dependency. Vue 3.2 and newer are supported.

Create a widget in the Cloudflare Turnstile dashboard, allow your hostnames, and keep the secret key on the server. The sitekey is public.

Basic Usage

<script setup lang="ts">
import { ref } from 'vue';
import {
  TurnstileWidget,
  type TurnstileWidgetInstance,
} from '@delaneydev/laravel-turnstile-vue';

const token = ref('');
const turnstile = ref<TurnstileWidgetInstance | null>(null);
const submitting = ref(false);

async function submit() {
  if (!token.value || submitting.value) return;
  submitting.value = true;

  try {
    const response = await fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ turnstileToken: token.value }),
    });
    if (!response.ok) throw new Error('Submission failed.');
  } finally {
    // Tokens are single-use. Reset after every request that may verify one.
    turnstile.value?.reset();
    submitting.value = false;
  }
}
</script>

<template>
  <form @submit.prevent="submit">
    <!-- Your fields -->
    <TurnstileWidget
      ref="turnstile"
      v-model="token"
      sitekey="your-public-sitekey"
      action="contact"
      size="flexible"
      :response-field="false"
    />
    <button type="submit" :disabled="!token || submitting">Send</button>
  </form>
</template>

Every token must be validated by your backend with Cloudflare Siteverify. Client-side success alone is not a security control. See server-side validation.

size="flexible" fills a container but keeps Cloudflare's 300-pixel minimum. Use size="compact" when the form's content box can be narrower.

Invisible And Deferred Challenges

Invisible mode is configured on the Cloudflare widget/sitekey. It is not a widget size. Pair an invisible sitekey with deferred execution:

<script setup lang="ts">
import { ref } from 'vue';
import {
  TurnstileWidget,
  type TurnstileWidgetInstance,
} from '@delaneydev/laravel-turnstile-vue';

const turnstile = ref<TurnstileWidgetInstance | null>(null);
const submitting = ref(false);

async function submit() {
  if (!turnstile.value || submitting.value) return;
  submitting.value = true;

  try {
    const token = await turnstile.value.challenge({
      force: true,
      timeoutMs: 30_000,
    });

    const response = await fetch('/api/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ turnstileToken: token }),
    });
    if (!response.ok) throw new Error('Submission failed.');
  } finally {
    // The request may have consumed the token even when it failed.
    turnstile.value?.reset();
    submitting.value = false;
  }
}
</script>

<template>
  <TurnstileWidget
    ref="turnstile"
    sitekey="your-invisible-sitekey"
    execution="execute"
    appearance="execute"
    action="checkout"
    :response-field="false"
  />
  <button type="button" :disabled="submitting" @click="submit">Pay</button>
</template>

challenge() returns an existing valid token or waits for a fresh one. Concurrent calls share one challenge. force: true resets a potentially consumed token first.

Cloudflare requires sites using Invisible Turnstile to reference its Turnstile Privacy Addendum.

Session And CSRF Preflight

Long-lived forms can outlive a server session. With deferred execution, use beforeExecute to refresh the session before asking the user to complete a challenge:

<TurnstileWidget
  ref="turnstile"
  sitekey="your-sitekey"
  execution="execute"
  appearance="execute"
  :before-execute="refreshSession"
/>

The hook runs before execute() and challenge(). If it rejects, no challenge starts and the component emits a typed preflight error. This avoids earning a short-lived, single-use token only to discover that the request will fail with an expired CSRF session. The package does not make framework-specific network requests on its own.

See the complete Laravel and Inertia session-refresh pattern.

SSR And Vite

The package can be imported and rendered on the server. It emits a stable empty container and loads Cloudflare only after Vue mounts in the browser.

Vite SSR should use its normal dependency externalization in version 2:

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  // No Turnstile-specific ssr.noExternal entry is required.
});

Both default externalization and a bundled ssr.noExternal build are tested against the packed npm artifact. The following was a version 1 workaround and can be removed after upgrading:

ssr: {
  noExternal: ['@delaneydev/laravel-turnstile-vue'],
}

See Laravel/Inertia SSR and the version 2 migration guide.

API At A Glance

The component supports all current Cloudflare options, including:

theme, language, tabindex, size, action, cData, retry, retryInterval, refreshExpired, refreshTimeout, appearance, execution, responseField, responseFieldName, feedbackEnabled, offlabelShowPrivacy, and offlabelShowHelp.

Package behavior is controlled with renderOnMount, injectScript, scriptOptions, resetOnModelClear, and beforeExecute.

Imperative methods:

render(): Promise<string>
execute(): Promise<void>
challenge(options?): Promise<string>
reset(): void
remove(): void
getResponse(): string | undefined
getResponsePromise(timeoutMs?): Promise<string>
isExpired(): boolean
getWidgetId(): string | undefined
getState(): TurnstileWidgetState

Events include success, error, expired, timeout, before-interactive, after-interactive, unsupported, loaded, widget-load, reset, and removed.

See the complete API reference.

Framework Guides

Browser Support

Turnstile's supported browsers and environments are controlled by Cloudflare. The component reports Cloudflare's unsupported-callback as an unsupported event and state. Do not block form recovery behind a widget that cannot load; provide a support or fallback path appropriate to your application.

Security

Report vulnerabilities through GitHub's private security advisory flow. Do not put a Turnstile secret key in Vue props, Vite VITE_* variables, browser code, logs, or the repository. See SECURITY.md.

License

This is a free and open source product from Delaney Industries Ltd, released under the MIT License. Contributions are welcome through GitHub issues and pull requests.

This project is independently developed and is not affiliated with, endorsed by, or sponsored by Cloudflare.

Related Packages

@gaviti/vue-turnstile

A Vue 3.3.x component wrapper for Cloudflare's Turnstile

21,878 8
@nuxtjs/turnstile

Cloudflare Turnstile integration for Nuxt

1,052,067 296
vue-turnstile

Vue library for Cloudflare Turnstile CAPTCHA alternative

992,136 92
@getjoystick/vue-turnstile

Vue library for Cloudflare Turnstile CAPTCHA alternative

577 0
@awes-io/nuxt-laravel

AwesCode UI module for connection with Laravel backend

7,075 9

Version History

Version Released Vue License
2.0.0 ^3.2.0 MIT
1.0.4 ^3.0.0 MIT
1.0.3 ^3.0.0 MIT
1.0.2 ^3.0.0 MIT
1.0.0 ^3.0.0 MIT