Skip to content

Inputs

Turnstile

Cloudflare's captcha widget that follows the theme and language, with getToken() and reset().

<script>
  import { Turnstile } from "@codeatlasdev/ui/forms";
  let handle = $state();
</script>

<Turnstile siteKey={PUBLIC_TURNSTILE_KEY} action="sign-in" bind:handle />
<!-- submit: handle.getToken(); failed submit: handle.reset() -->

It lives in @codeatlasdev/ui/forms, adds no dependency and loads Cloudflare’s script on demand, once per page (never on the server). It follows the colour scheme and UI language, re-renders when either changes and removes the widget on unmount. Without a siteKey it renders and loads nothing and getToken() is undefined. A token is single-use and only counts once your server verifies it: call reset() after a failed submit. Strict CSP: pass nonce, allow challenges.cloudflare.com.

#843

Sign-in form

Cloudflare's always-pass test key.

Reference #843 · Pick in the gallery

Code
svelte
<script lang="ts">
  import { Button, Stack, TextField } from '@codeatlasdev/ui';
  import { Turnstile, type TurnstileHandle } from '@codeatlasdev/ui/forms';

  // Cloudflare's public test key: always passes. Use your own site key in production.
  const SITE_KEY = '1x00000000000000000000AA';

  let handle = $state<TurnstileHandle>();
  let email = $state('');
  let status = $state('');

  async function submit(e: SubmitEvent) {
    e.preventDefault();
    const token = handle?.getToken();
    if (!token) {
      status = 'Complete the security check first.';
      return;
    }
    // POST { email, token } and verify the token on your server. A token is single-use:
    // after a failed submit start a new challenge.
    status = `Sending with a ${token.length}-character token…`;
    handle?.reset();
  }
</script>

<form onsubmit={submit}>
  <Stack gap="3">
    <TextField label="Email" type="email" bind:value={email} />
    <Turnstile siteKey={SITE_KEY} action="sign-in" bind:handle />
    <Button type="submit" fullWidth>Sign in</Button>
    <p class="status" role="status">{status}</p>
  </Stack>
</form>

<style>
  form { width: 100%; max-width: 20rem; }
  .status { margin: 0; color: var(--ca-text-secondary); font-size: var(--ca-font-size-caption); }
</style>

Props

PropTypeDefaultDescription
siteKeystringundefinedPublic site key. Unset: nothing renders or loads.
actionstringundefinedFlow label, echoed when you verify.
cDatastringundefinedOpaque data, echoed when you verify.
theme'auto' | 'light' | 'dark''auto'`auto` follows the active colour scheme.
languagestringactive localeLanguage tag; the UI locale in lower case (`pt-br`).
size'normal' | 'flexible' | 'compact'undefinedWidget size.
appearance'always' | 'execute' | 'interaction-only'undefinedWhen it is visible.
handleTurnstileHandlebindable`getToken()` and `reset()` (new challenge, clears the token).
tokenstringbindableThe current token, `undefined` while there is none.
onverify(token: string) => voidundefinedCalled when the check passes.
onexpire() => voidundefinedCalled when the token expires; the token is cleared.
onerror(code: string) => voidundefinedWidget or script failed; `code` is Cloudflare's.
noncestringundefinedCSP nonce for the script tag.
classstring''Extra class names.