@letar/forms

CAPTCHA

Bot protection with Cloudflare Turnstile, Google reCAPTCHA, hCaptcha, or Yandex SmartCaptcha

Full example

The complete sandbox example, read directly from the form-develop-app source at build time — switch framework/skin to see Form.Captcha wired to a real form. No shadcn/Vue/Angular equivalent exists yet — those tabs show as disabled.

apps/form-develop-app/src/app/captcha-demo/page.tsx
'use client'

import { Box, Code, Heading, Text, VStack } from '@chakra-ui/react'
import { DemoPageLayout } from '../_components'

export default function CaptchaDemoPage() {
  return (
    <DemoPageLayout
      title="Form.Captcha"
      description="CAPTCHA виджет — Turnstile, reCAPTCHA, hCaptcha, Yandex SmartCaptcha"
    >
      <VStack gap={8} align="stretch">
        <Box>
          <Heading size="md" mb={3}>
            Использование
          </Heading>
          <Text fontSize="sm" color="fg.muted" mb={4}>
            CAPTCHA требует настоящий siteKey от провайдера. Ниже — API и примеры конфигурации.
          </Text>
          <Code display="block" whiteSpace="pre" fontSize="xs" p={4} borderRadius="md">
            {`// В createForm (один раз для приложения)
const AppForm = createForm({
  captcha: {
    provider: 'turnstile',
    siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!,
    theme: 'auto',
  },
})

// В форме
<AppForm onSubmit={handleSubmit}>
  <AppForm.Field.String name="email" label="Email" />
  <AppForm.Captcha />
  <AppForm.Button.Submit>Отправить</AppForm.Button.Submit>
</AppForm>

// Серверная верификация
import { verifyCaptcha } from '@letar/forms/captcha/server'

const result = await verifyCaptcha(token, {
  provider: 'turnstile',
  secretKey: process.env.TURNSTILE_SECRET_KEY!,
})`}
          </Code>
        </Box>

        <Box>
          <Heading size="md" mb={3}>
            Провайдеры
          </Heading>
          <VStack align="start" gap={2} fontSize="sm">
            <Text>
              <strong>turnstile</strong> — Cloudflare Turnstile (рекомендуемый, бесплатный)
            </Text>
            <Text>
              <strong>recaptcha</strong> — Google reCAPTCHA v2/v3
            </Text>
            <Text>
              <strong>hcaptcha</strong> — hCaptcha
            </Text>
            <Text>
              <strong>smartcaptcha</strong> — Yandex SmartCaptcha (данные обрабатываются в РФ, для 152-ФЗ)
            </Text>
          </VStack>
        </Box>
      </VStack>
    </DemoPageLayout>
  )
}

Overview

Form.Captcha adds bot protection to forms. Supports four providers: Cloudflare Turnstile (recommended, free), Google reCAPTCHA, hCaptcha, and Yandex SmartCaptcha (data stays in Russia — useful for 152-FZ compliance).

Setup

Configure CAPTCHA once in createForm:

const AppForm = createForm({
  captcha: {
    provider: 'turnstile',
    siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!,
    theme: 'auto',
  },
})

Usage in Form

<AppForm onSubmit={handleSubmit}>
  <AppForm.Field.String name="email" label="Email" />
  <AppForm.Field.Textarea name="message" label="Message" />
  <AppForm.Captcha />
  <AppForm.Button.Submit>Send</AppForm.Button.Submit>
</AppForm>

The CAPTCHA token is automatically included in the form data as __captchaToken.

Server Verification

import { verifyCaptcha } from '@letar/forms/captcha/server'

export async function submitAction(data: FormData) {
  const token = data.__captchaToken
  const result = await verifyCaptcha(token, {
    provider: 'turnstile',
    secretKey: process.env.TURNSTILE_SECRET_KEY!,
  })

  if (!result.success) {
    throw new Error('CAPTCHA verification failed')
  }
  // Process form...
}

Providers

ProviderFreeInvisibleNotes
TurnstileYesYesRecommended, privacy-friendly
reCAPTCHAPartialv3 onlyGoogle, most popular
hCaptchaPartialYesPrivacy alternative to reCAPTCHA
SmartCaptchaYesYesYandex, data processed in Russia (152-FZ). No theme support.

Props

PropTypeDefaultDescription
provider'turnstile' | 'recaptcha' | 'hcaptcha' | 'smartcaptcha'from createFormOverride provider
siteKeystringfrom createFormOverride siteKey
theme'auto' | 'light' | 'dark''auto'Widget theme
size'normal' | 'compact' | 'invisible''normal'Widget size
languagestringLanguage code (ISO 639-1)
onSuccess(token: string) => voidSuccess callback
onError(error) => voidError callback
onExpire() => voidToken expiry callback

Live Example

Try the interactive example on forms-example.letar.best.

On this page