diff --git a/.editorconfig b/.editorconfig
index db9b162..0d9169b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -3,11 +3,8 @@
root = true
[*]
-indent_style = tab
+indent_style = space
tab_width = 2
end_of_line = lf
insert_final_newline = true
-[*.{yml,yaml}]
-indent_style = space
-indent_size = 2
diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index 26ccf79..0000000
--- a/.eslintignore
+++ /dev/null
@@ -1,5 +0,0 @@
-node_modules/
-dist/
-build/
-coverage/
-.next/
diff --git a/.github/workflows/check-skills.yml b/.github/workflows/check-skills.yml
new file mode 100644
index 0000000..b2f9812
--- /dev/null
+++ b/.github/workflows/check-skills.yml
@@ -0,0 +1,54 @@
+name: Check Stale Skills
+
+on:
+ release:
+ types: [published]
+ workflow_dispatch:
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+
+ - name: Set node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24.x
+ cache: pnpm
+
+ - name: Setup
+ run: npm i -g @antfu/ni
+
+ - name: Install
+ run: nci
+
+ - name: Check for stale skills
+ id: stale
+ run: |
+ cd packages/lib
+ npx @tanstack/intent@latest stale --json > stale-report.json || true
+
+ - name: Create PR if skills are stale
+ if: steps.stale.outcome == 'failure'
+ uses: peter-evans/create-pull-request@v5
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ commit-message: 'chore: update stale skills'
+ title: 'Update stale skills'
+ body: |
+ This PR was created because the following skills may be out of date:
+
+ Please review and update the skills using:
+ ```
+ npx @tanstack/intent@latest scaffold
+ ```
+
+ Then run validation:
+ ```
+ npx @tanstack/intent@latest validate
+ ```
+ branch: update-stale-skills
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 93e2e3c..5414791 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,7 +20,7 @@ on:
- "**.mdx"
jobs:
- lint:
+ check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -43,60 +43,8 @@ jobs:
- name: Build
run: nr lib:build
- - name: Lint
- run: nr lint --cache
-
- format:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
-
- - name: Set node
- uses: actions/setup-node@v4
- with:
- node-version: 24.x
- cache: pnpm
-
- - name: Setup
- run: npm i -g @antfu/ni
-
- - name: Install
- run: nci
-
- - name: Build
- run: nr lib:build
-
- - name: Format
- run: nr format:check --cache
-
- type-check:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
-
- - name: Set node
- uses: actions/setup-node@v4
- with:
- node-version: 24.x
- cache: pnpm
-
- - name: Setup
- run: npm i -g @antfu/ni
-
- - name: Install
- run: nci
-
- - name: Build
- run: nr lib:build
-
- - name: Type check
- run: nr type-check
+ - name: Check
+ run: nr check
e2e-test:
runs-on: ubuntu-latest
diff --git a/.github/workflows/validate-skills.yml b/.github/workflows/validate-skills.yml
new file mode 100644
index 0000000..09055ab
--- /dev/null
+++ b/.github/workflows/validate-skills.yml
@@ -0,0 +1,38 @@
+name: Validate Skills
+
+on:
+ pull_request:
+ paths:
+ - 'packages/lib/skills/**'
+ - 'packages/lib/_artifacts/**'
+ push:
+ branches: [main]
+ paths:
+ - 'packages/lib/skills/**'
+ - 'packages/lib/_artifacts/**'
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+
+ - name: Set node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24.x
+ cache: pnpm
+
+ - name: Setup
+ run: npm i -g @antfu/ni
+
+ - name: Install
+ run: nci
+
+ - name: Validate skills
+ run: |
+ cd packages/lib
+ npx @tanstack/intent@latest validate
diff --git a/.markdownlint.json b/.markdownlint.json
deleted file mode 100644
index 9bc533b..0000000
--- a/.markdownlint.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "default": true,
- "no-blanks-blockquote": false,
- "line-length": false,
- "no-inline-html": false,
- "first-line-h1": false
-}
diff --git a/.prettierignore b/.prettierignore
deleted file mode 100644
index 66425c6..0000000
--- a/.prettierignore
+++ /dev/null
@@ -1,8 +0,0 @@
-.vscode
-dist
-.next
-*.yaml
-*.yml
-*.css
-*.md
-*.mdx
diff --git a/.prettierrc.mjs b/.prettierrc.mjs
deleted file mode 100644
index 8f88989..0000000
--- a/.prettierrc.mjs
+++ /dev/null
@@ -1,11 +0,0 @@
-export default {
- semi: false,
- useTabs: true,
- singleQuote: true,
- jsxSingleQuote: false,
- trailingComma: 'none',
- tabWidth: 2,
- arrowParens: 'avoid',
- printWidth: 100,
- plugins: ['prettier-plugin-tailwindcss']
-}
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 3e032b1..4613b16 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,3 +1,7 @@
{
- "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"],
+ "recommendations": ["oxc.oxc-vscode", "typescriptteam.native-preview"],
+ "unwantedRecommendations": [
+ "dbaeumer.vscode-eslint",
+ "esbenp.prettier-vscode"
+ ]
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 977a1ca..468f65c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,25 +1,12 @@
{
- "eslint.workingDirectories": [
- "./packages/lib",
- "./demos/nextjs"
- ],
- "editor.codeActionsOnSave": {
- "source.fixAll.eslint": "explicit"
- },
+ "editor.defaultFormatter": "oxc.oxc-vscode",
"editor.formatOnSave": true,
- "eslint.options": {
- "extensions": [
- ".js",
- ".ts",
- ".tsx"
- ]
+ "editor.codeActionsOnSave": {
+ "source.fixAll.oxc": "always"
},
- "eslint.validate": [
- "javascript",
- "javascriptreact",
- "typescript",
- "typescriptreact"
- ],
+ "oxc.fmt.configPath": "oxfmt.config.ts",
"editor.insertSpaces": false,
- "editor.detectIndentation": false
-}
\ No newline at end of file
+ "editor.detectIndentation": false,
+ "typescript.experimental.useTsgo": true,
+ "js/ts.experimental.useTsgo": true
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..0730157
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,168 @@
+# Contributing to React Turnstile
+
+Thank you for your interest in contributing! This guide will help you get started with our development workflow.
+
+## Prerequisites
+
+- **Node.js 24+** (we recommend using [nvm](https://github.com/nvm-sh/nvm))
+- **pnpm** (package manager - will be auto-installed via corepack)
+
+## Quick Start
+
+```bash
+# Clone the repository
+git clone https://github.com/marsidev/react-turnstile.git
+cd react-turnstile
+
+# Install dependencies
+pnpm install
+
+# Start development server (builds library with HMR + starts Next.js demo)
+pnpm dev
+```
+
+The dev server will start:
+
+- Library build with hot module replacement on port (via tsup)
+- Next.js demo app on port 3333
+
+Visit [http://localhost:3333](http://localhost:3333) to see the demo.
+
+## Development Workflow
+
+### TypeScript v7 (Go Implementation)
+
+We use **TypeScript v7** (currently in preview, written in Go) for type-checking and type-aware linting. This provides significant performance improvements over traditional TypeScript.
+
+```bash
+# Build the library first (required for type-aware linting)
+pnpm lib:build
+
+# Run type checking with tsgo
+pnpm typecheck
+```
+
+**Important:** Type-aware linting requires built `.d.ts` files for cross-package type resolution in our monorepo setup. Always build before running type checks in CI.
+
+### Linting and Formatting
+
+We use **Oxlint** and **Oxfmt** for linting and formatting.
+
+```bash
+# Fix all linting and formatting issues
+pnpm fix
+
+# Check without modifying files (for CI)
+pnpm check
+
+# Individual commands
+pnpm lint # Fix linting issues
+pnpm lint:check # Check linting only
+pnpm format # Format files
+pnpm format:check # Check formatting only
+pnpm typecheck # Type check all packages
+```
+
+### Running Tests
+
+```bash
+# Run all tests
+pnpm test
+
+# Integration tests only
+pnpm test:integration
+
+# E2E tests only
+pnpm test:e2e
+```
+
+### Pre-commit Hooks
+
+We use `simple-git-hooks` to run checks before each commit. This ensures code quality before pushing.
+
+```bash
+# The following runs automatically on git commit:
+pnpm check # lint:check + format:check + typecheck
+```
+
+## Project Structure
+
+```
+.
+├── packages/lib/ # Library source code (@marsidev/react-turnstile)
+│ ├── src/ # Source code
+│ ├── dist/ # Build output
+│ └── package.json # Library package config
+├── demos/nextjs/ # Next.js demo app
+├── docs/ # Documentation
+└── test/ # E2E tests (Playwright)
+```
+
+## Making Changes
+
+1. **Fork** the [repository](https://github.com/marsidev/react-turnstile/fork) and clone it
+2. **Create a branch** from `main` for your changes
+3. **Make your changes** with clear, focused commits
+4. **Run checks** before committing: `pnpm check`
+5. **Test your changes** with the demo app: `pnpm dev`
+6. **Submit a PR** with a clear description
+
+## Resources
+
+- **[📖 Documentation](https://docs.page/marsidev/react-turnstile/)**
+- **[🚀 Live Demo](https://react-turnstile.vercel.app/)**
+- **[📦 NPM Package](https://npm.im/@marsidev/react-turnstile)**
+
+## Editor Setup
+
+### VS Code (or any editor based on VS Code)
+
+Install the recommended extensions (you'll be prompted):
+
+- **Oxc** (`oxc.oxc-vscode`) - Linting and formatting
+- **TypeScript Native Preview** (`typescriptteam.native-preview`) - TypeScript v7 support
+
+These extensions will provide:
+
+- Auto-formatting on save
+- Real-time linting
+- Type errors
+- Import sorting
+
+## Troubleshooting
+
+### Type-aware linting is slow
+
+Enable debug logging to see which files are causing issues:
+
+```bash
+OXC_LOG=debug pnpm lint:check
+```
+
+Look for programs with unusually high file counts in the output.
+
+### Build fails with type errors
+
+Make sure you've built the library first:
+
+```bash
+pnpm lib:build
+pnpm typecheck
+```
+
+### Pre-commit hooks not running
+
+Reinstall hooks:
+
+```bash
+pnpm simple-git-hooks
+```
+
+## Questions?
+
+- Open an [issue](https://github.com/marsidev/react-turnstile/issues/new)
+- Check existing [discussions](https://github.com/marsidev/react-turnstile/discussions)
+
+## License
+
+By contributing, you agree that your contributions will be licensed under the MIT License.
diff --git a/README.md b/README.md
index 1d1a85e..3fd28c7 100644
--- a/README.md
+++ b/README.md
@@ -11,26 +11,23 @@
-
-
+
+
-
-
-
-
## Features
-* 💪 Smart verification with minimal user interaction
-* 🕵️♀️ Privacy-focused approach
-* 💉 Automatic script injection
-* ⚡️ SSR ready
-* 💻 TypeScript support
+* 🕵️♀️ **Privacy-first** - No user tracking, no cookies, GDPR-friendly by design
+* 📦 **Lightweight** - Only 6.3 KB minified
+* 💪 **Smart verification** - Often invisible to users with minimal interaction
+* 💉 **Zero config** - Automatic script injection, works out of the box
+* 📐 **Multiple widget sizes** - normal, compact, flexible, invisible
+* 🎮 **Imperative API** - Control via ref: `reset()`, `execute()`, `getResponse()`, `isExpired()`
+* ⚡️ **SSR ready** - Works with Next.js, Remix, and any React SSR framework
+* 💻 **Full TypeScript support**
-## [Docs](https://docs.page/marsidev/react-turnstile/) | [Demo](https://react-turnstile.vercel.app/)
+### **[📖 Read the docs →](https://docs.page/marsidev/react-turnstile/)** **[🚀 See the demo →](https://react-turnstile.vercel.app/)**
## Getting started
@@ -38,11 +35,13 @@
2. Install `@marsidev/react-turnstile` into your React project.
```bash
- npm i @marsidev/react-turnstile
+ pnpm add @marsidev/react-turnstile
```
## Usage
+### Basic
+
```jsx
import { Turnstile } from '@marsidev/react-turnstile'
@@ -51,9 +50,36 @@ function Widget() {
}
```
-> Checkout [the docs](https://docs.page/marsidev/react-turnstile) for more examples and for a detailed info about the `Turnstile` props.
+### With form and imperative API
-> Checkout [the demo](https://react-turnstile.vercel.app/) for a live example.
+```tsx
+import { Turnstile } from '@marsidev/react-turnstile'
+import type { TurnstileInstance } from '@marsidev/react-turnstile'
+import { useRef } from 'react'
+
+function LoginForm() {
+ const turnstileRef = useRef(null)
+
+ async function handleSubmit(e: FormEvent) {
+ e.preventDefault()
+ const token = turnstileRef.current?.getResponse()
+ // Send token to your server...
+ turnstileRef.current?.reset() // Reset after submission
+ }
+
+ return (
+
+ )
+}
+```
## Contributing
@@ -63,28 +89,28 @@ Any contributions are greatly appreciated. If you have a suggestion that would m
* [Fork](https://github.com/marsidev/react-turnstile/fork) or clone this repository.
* Install dependencies with `pnpm install`.
-* You can use `pnpm dev` to start the demo page in dev mode, which also rebuild the library when file changes are detected in the `packages/lib` folder.
+* Run `pnpm dev` to start both the library (with HMR) and the Next.js demo app concurrently
-> The library is written under the `packages/lib` folder, the demo page is under the `demos/nextjs` folder and the docs are under the `docs` folder.
+For reference, the project structure is as follows:
-## Contributors
-
-
-
-
+```
+.
+├── packages/lib/ # Library source code
+├── demos/nextjs/ # Next.js demo app
+└── docs/ # Documentation
+```
-## Credits
+## Support
-Inspired by
+If you find this library useful, you can buy me a coffee:
-* [nuxt-turnstile](https://github.com/danielroe/nuxt-turnstile)
-* [svelte-turnstile](https://github.com/ghostdevv/svelte-turnstile)
-* [react-google-recaptcha-v3](https://github.com/t49tran/react-google-recaptcha-v3)
-* [reaptcha](https://github.com/sarneeh/reaptcha)
+
-## Support
+## Contributors
-If you like this project, please consider supporting it through a [PayPal donation](https://paypal.me/marsigliacr). :blush:
+
+
+
## License
diff --git a/demos/nextjs/.vscode/settings.json b/demos/nextjs/.vscode/settings.json
deleted file mode 100644
index 82f4373..0000000
--- a/demos/nextjs/.vscode/settings.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "typescript.tsdk": "..\\..\\node_modules\\.pnpm\\typescript@4.9.5\\node_modules\\typescript\\lib",
- "typescript.enablePromptUseWorkspaceTsdk": true
-}
\ No newline at end of file
diff --git a/demos/nextjs/global.d.ts b/demos/nextjs/global.d.ts
new file mode 100644
index 0000000..cbe652d
--- /dev/null
+++ b/demos/nextjs/global.d.ts
@@ -0,0 +1 @@
+declare module "*.css";
diff --git a/demos/nextjs/next.config.js b/demos/nextjs/next.config.js
index c4c0f65..ecb29da 100644
--- a/demos/nextjs/next.config.js
+++ b/demos/nextjs/next.config.js
@@ -1,19 +1,19 @@
-const path = require('node:path')
+const path = require("node:path");
/** @type {import('next').NextConfig} */
const nextConfig = {
- turbopack: {
- root: path.join(__dirname, '../..')
- },
- async redirects() {
- return [
- {
- source: '/',
- destination: '/basic',
- permanent: true
- }
- ]
- }
-}
+ turbopack: {
+ root: path.join(__dirname, "../..")
+ },
+ async redirects() {
+ return [
+ {
+ source: "/",
+ destination: "/basic",
+ permanent: true
+ }
+ ];
+ }
+};
-module.exports = nextConfig
+module.exports = nextConfig;
diff --git a/demos/nextjs/package.json b/demos/nextjs/package.json
index 6d7ed8b..7667ecb 100644
--- a/demos/nextjs/package.json
+++ b/demos/nextjs/package.json
@@ -1,31 +1,31 @@
{
- "name": "nextjs-demo",
- "version": "0.0.1",
- "private": true,
- "scripts": {
- "dev": "next dev --port 3333",
- "build": "pnpm run --filter=@marsidev/react-turnstile build && next build",
- "start": "next start --port 3333",
- "type-check": "tsc --noEmit"
- },
- "dependencies": {
- "@marsidev/react-turnstile": "workspace:*",
- "@radix-ui/react-select": "2.2.6",
- "class-variance-authority": "0.7.1",
- "classnames": "2.5.1",
- "jotai": "2.17.0",
- "lucide-react": "0.563.0",
- "next": "16.1.6",
- "next-themes": "0.4.6",
- "react": "19.2.4",
- "react-dom": "19.2.4",
- "tailwind-merge": "3.4.0"
- },
- "devDependencies": {
- "@tailwindcss/postcss": "4.1.18",
- "@tailwindcss/typography": "0.5.19",
- "autoprefixer": "10.4.24",
- "postcss": "8.5.6",
- "tailwindcss": "4.1.18"
- }
+ "name": "nextjs-demo",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "next dev --port 3333",
+ "build": "pnpm run --filter=@marsidev/react-turnstile build && next build",
+ "start": "next start --port 3333",
+ "typecheck": "tsgo --noEmit"
+ },
+ "dependencies": {
+ "@marsidev/react-turnstile": "workspace:*",
+ "@radix-ui/react-select": "2.2.6",
+ "class-variance-authority": "0.7.1",
+ "classnames": "2.5.1",
+ "jotai": "2.17.0",
+ "lucide-react": "0.563.0",
+ "next": "16.1.6",
+ "next-themes": "0.4.6",
+ "react": "19.2.4",
+ "react-dom": "19.2.4",
+ "tailwind-merge": "3.4.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "4.1.18",
+ "@tailwindcss/typography": "0.5.19",
+ "autoprefixer": "10.4.24",
+ "postcss": "8.5.6",
+ "tailwindcss": "4.1.18"
+ }
}
diff --git a/demos/nextjs/postcss.config.js b/demos/nextjs/postcss.config.js
index 0b295e4..05856bf 100644
--- a/demos/nextjs/postcss.config.js
+++ b/demos/nextjs/postcss.config.js
@@ -1,5 +1,5 @@
module.exports = {
- plugins: {
- '@tailwindcss/postcss': {}
- }
-}
+ plugins: {
+ "@tailwindcss/postcss": {}
+ }
+};
diff --git a/demos/nextjs/src/app/api/verify/route.ts b/demos/nextjs/src/app/api/verify/route.ts
index 6f67ae6..847b9e7 100644
--- a/demos/nextjs/src/app/api/verify/route.ts
+++ b/demos/nextjs/src/app/api/verify/route.ts
@@ -1,32 +1,32 @@
-import type { TurnstileServerValidationResponse } from '@marsidev/react-turnstile'
+import type { TurnstileServerValidationResponse } from "@marsidev/react-turnstile";
-const verifyEndpoint = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'
+const verifyEndpoint = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
const responseHeaders = {
- 'content-type': 'application/json'
-}
+ "content-type": "application/json"
+};
export async function POST(request: Request) {
- const body = (await request.json()) as { token: string; secret: string }
- const { token, secret } = body
+ const body = (await request.json()) as { token: string; secret: string };
+ const { token, secret } = body;
- const data = (await fetch(verifyEndpoint, {
- method: 'POST',
- body: `secret=${encodeURIComponent(secret)}&response=${encodeURIComponent(token)}`,
- headers: {
- 'content-type': 'application/x-www-form-urlencoded'
- }
- }).then(res => res.json())) as TurnstileServerValidationResponse
+ const data = (await fetch(verifyEndpoint, {
+ method: "POST",
+ body: `secret=${encodeURIComponent(secret)}&response=${encodeURIComponent(token)}`,
+ headers: {
+ "content-type": "application/x-www-form-urlencoded"
+ }
+ }).then(res => res.json())) as TurnstileServerValidationResponse;
- if (!data.success) {
- return new Response(JSON.stringify(data), {
- status: 400,
- headers: responseHeaders
- })
- }
+ if (!data.success) {
+ return new Response(JSON.stringify(data), {
+ status: 400,
+ headers: responseHeaders
+ });
+ }
- return new Response(JSON.stringify(data), {
- status: 200,
- headers: responseHeaders
- })
+ return new Response(JSON.stringify(data), {
+ status: 200,
+ headers: responseHeaders
+ });
}
diff --git a/demos/nextjs/src/app/basic/page.tsx b/demos/nextjs/src/app/basic/page.tsx
index 544f89d..728faa7 100644
--- a/demos/nextjs/src/app/basic/page.tsx
+++ b/demos/nextjs/src/app/basic/page.tsx
@@ -1,13 +1,13 @@
-'use client'
+"use client";
-import React from 'react'
-import DemoWidget from '~/components/demo-widget'
+import React from "react";
+import DemoWidget from "~/components/demo-widget";
export default function Page() {
- return (
-
-
Basic demo
-
-
- )
+ return (
+
+
Basic demo
+
+
+ );
}
diff --git a/demos/nextjs/src/app/layout.tsx b/demos/nextjs/src/app/layout.tsx
index 2cdde53..c90dd0c 100644
--- a/demos/nextjs/src/app/layout.tsx
+++ b/demos/nextjs/src/app/layout.tsx
@@ -1,31 +1,31 @@
-import '~/app/globals.css'
-import { Inter } from 'next/font/google'
-import Header from '~/components/header'
-import Sidebar from '~/components/sidebar'
-import Content from '~/components/content'
-import ThemeProvider from '~/components/theme-provider'
-import { cn } from '~/utils'
+import "~/app/globals.css";
+import { Inter } from "next/font/google";
+import Header from "~/components/header";
+import Sidebar from "~/components/sidebar";
+import Content from "~/components/content";
+import ThemeProvider from "~/components/theme-provider";
+import { cn } from "~/utils";
-const inter = Inter({ subsets: ['latin'] })
+const inter = Inter({ subsets: ["latin"] });
export const metadata = {
- title: 'React Turnstile Demo',
- description: 'React Turnstile Next.js 16 App Router Demo'
-}
+ title: "React Turnstile Demo",
+ description: "React Turnstile Next.js 16 App Router Demo"
+};
export default function RootLayout({ children }: React.PropsWithChildren) {
- return (
-
-
-
-
-
-
+ return (
+
+
+
+
+
+
- {children}
-
-
-
-
- )
+ {children}
+
+
+
+
+ );
}
diff --git a/demos/nextjs/src/app/manual-script-injection-with-custom-script-props/page.tsx b/demos/nextjs/src/app/manual-script-injection-with-custom-script-props/page.tsx
index af8ae24..95a458e 100644
--- a/demos/nextjs/src/app/manual-script-injection-with-custom-script-props/page.tsx
+++ b/demos/nextjs/src/app/manual-script-injection-with-custom-script-props/page.tsx
@@ -1,24 +1,24 @@
-'use client'
+"use client";
-import Script from 'next/script'
-import { SCRIPT_URL } from '@marsidev/react-turnstile'
-import React from 'react'
-import DemoWidget from '~/components/demo-widget'
+import Script from "next/script";
+import { SCRIPT_URL } from "@marsidev/react-turnstile";
+import React from "react";
+import DemoWidget from "~/components/demo-widget";
export default function Page() {
- return (
-
- {/* We add a custom query param to the script URL to force a re-download of the script, since the manual script injection is also used in other demos. This is not needed if the script ID is the same. */}
-
+ return (
+
+ {/* We add a custom query param to the script URL to force a re-download of the script, since the manual script injection is also used in other demos. This is not needed if the script ID is the same. */}
+
-
Manual script injection with custom script props
+
Manual script injection with custom script props
-
-
- )
+
+
+ );
}
diff --git a/demos/nextjs/src/app/manual-script-injection/page.tsx b/demos/nextjs/src/app/manual-script-injection/page.tsx
index f09006b..d7882d2 100644
--- a/demos/nextjs/src/app/manual-script-injection/page.tsx
+++ b/demos/nextjs/src/app/manual-script-injection/page.tsx
@@ -1,17 +1,17 @@
-'use client'
+"use client";
-import Script from 'next/script'
-import { DEFAULT_SCRIPT_ID, SCRIPT_URL } from '@marsidev/react-turnstile'
-import React from 'react'
-import DemoWidget from '~/components/demo-widget'
+import Script from "next/script";
+import { DEFAULT_SCRIPT_ID, SCRIPT_URL } from "@marsidev/react-turnstile";
+import React from "react";
+import DemoWidget from "~/components/demo-widget";
export default function Page() {
- return (
-
-
+ return (
+
+
-
Manual script injection
-
-
- )
+
Manual script injection
+
+
+ );
}
diff --git a/demos/nextjs/src/app/multi-widgets-and-manual-injection/page.tsx b/demos/nextjs/src/app/multi-widgets-and-manual-injection/page.tsx
index 0112e9a..9e450c8 100644
--- a/demos/nextjs/src/app/multi-widgets-and-manual-injection/page.tsx
+++ b/demos/nextjs/src/app/multi-widgets-and-manual-injection/page.tsx
@@ -1,22 +1,22 @@
-'use client'
+"use client";
-import Script from 'next/script'
-import { DEFAULT_SCRIPT_ID, SCRIPT_URL } from '@marsidev/react-turnstile'
-import React from 'react'
-import DemoWidget from '~/components/demo-widget'
+import Script from "next/script";
+import { DEFAULT_SCRIPT_ID, SCRIPT_URL } from "@marsidev/react-turnstile";
+import React from "react";
+import DemoWidget from "~/components/demo-widget";
export default function Page() {
- return (
-
-
+ return (
+
+
-
Multiple widgets with manual script injection
+
Multiple widgets with manual script injection
-
Widget 1
-
+
Widget 1
+
-
Widget 2
-
-
- )
+
Widget 2
+
+
+ );
}
diff --git a/demos/nextjs/src/app/multiple-widgets/page.tsx b/demos/nextjs/src/app/multiple-widgets/page.tsx
index 7d4edd4..b286558 100644
--- a/demos/nextjs/src/app/multiple-widgets/page.tsx
+++ b/demos/nextjs/src/app/multiple-widgets/page.tsx
@@ -1,18 +1,18 @@
-'use client'
+"use client";
-import React from 'react'
-import DemoWidget from '~/components/demo-widget'
+import React from "react";
+import DemoWidget from "~/components/demo-widget";
export default function Page() {
- return (
-
-
+ )
+}
+```
+
+## Widget Sizes
+
+Choose the size that fits your layout:
+
+| Size | Dimensions | Best For |
+|------|------------|----------|
+| `normal` (default) | 300×65px | Most forms, standard layouts |
+| `compact` | 150×140px | Mobile, tight spaces |
+| `flexible` | 100% width × 65px | Responsive layouts |
+| `invisible` | 0×0px | Hidden widgets (requires Invisible widget type from Cloudflare) |
+
+```tsx
+// Compact size for mobile
+
+
+// Flexible width
+
+```
+
+## Handling the Success Token
+
+Use the `onSuccess` callback to receive the verification token:
+
+```tsx
+import { Turnstile } from '@marsidev/react-turnstile'
+import { useState } from 'react'
+
+export default function LoginForm() {
+ const [token, setToken] = useState(null)
+
+ return (
+
+
+
+
+ setToken(token)}
+ />
+
+
+
+ )
+}
+```
+
+## Complete Form Example
+
+```tsx
+'use client' // For Next.js App Router
+import { Turnstile } from '@marsidev/react-turnstile'
+import { useState } from 'react'
+
+export default function ContactForm() {
+ const [token, setToken] = useState(null)
+ const [isSubmitting, setIsSubmitting] = useState(false)
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault()
+ if (!token) return
+
+ setIsSubmitting(true)
+
+ // Send token to your server for validation
+ const response = await fetch('/api/contact', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ token, /* other form data */ })
+ })
+
+ if (response.ok) {
+ // Handle success
+ }
+
+ setIsSubmitting(false)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
+```
+
+## Common Mistakes
+
+### ❌ Using Invisible Size Without Invisible Widget Type
+
+The `invisible` size is **only** for the Invisible widget type from Cloudflare. Using it with a normal widget shows nothing.
+
+**Wrong:**
+```tsx
+// Shows nothing if widget type is not "Invisible"
+
+```
+
+**Correct:**
+```tsx
+// Use visible sizes for normal widgets
+
+
+
+```
+
+## Next Steps
+
+- **Customize appearance:** See [widget-customization skill](./widget-customization/SKILL.md)
+- **Handle tokens properly:** See [token-lifecycle skill](./token-lifecycle/SKILL.md)
+- **Next.js integration:** See [nextjs-ssr skill](./nextjs-ssr/SKILL.md)
+- **Multiple widgets:** See [multiple-widgets skill](./multiple-widgets/SKILL.md)
+
+## Testing During Development
+
+Use Cloudflare's test keys that always pass validation:
+
+- **Site key:** `1x00000000000000000000AA`
+- **Secret key:** `1x0000000000000000000000000000000AA`
+
+See: https://developers.cloudflare.com/turnstile/troubleshooting/testing/
diff --git a/packages/lib/skills/multiple-widgets/SKILL.md b/packages/lib/skills/multiple-widgets/SKILL.md
new file mode 100644
index 0000000..3193a6f
--- /dev/null
+++ b/packages/lib/skills/multiple-widgets/SKILL.md
@@ -0,0 +1,375 @@
+---
+name: multiple-widgets
+description: >
+ Handle multiple Turnstile widgets on the same page without conflicts.
+ Activate when adding more than one CAPTCHA to a page, or when widgets
+ interfere with each other.
+triggers:
+ - 'multiple turnstile widgets'
+ - 'two turnstile forms'
+ - 'turnstile widget conflict'
+ - 'turnstile race condition'
+ - 'turnstile id'
+ - 'multiple captcha same page'
+ - 'widget not responding'
+category: core
+metadata:
+ library: '@marsidev/react-turnstile'
+ library_version: '1.4.2'
+ framework: React
+---
+
+# Multiple Widgets
+
+Handle multiple Turnstile widgets on the same page without conflicts or race conditions.
+
+## Basic Multiple Widgets
+
+When using multiple widgets, always provide unique IDs:
+
+```tsx
+import { Turnstile } from '@marsidev/react-turnstile'
+
+export default function MultiFormPage() {
+ return (
+ <>
+
+
Newsletter Signup
+
+
+
+
+
+
+
+
+
Contact Form
+
+
+
+
+
+
+
+ >
+ )
+}
+```
+
+## With Separate Refs
+
+Control each widget independently:
+
+```tsx
+import { Turnstile } from '@marsidev/react-turnstile'
+import type { TurnstileInstance } from '@marsidev/react-turnstile'
+import { useRef } from 'react'
+
+export default function MultiFormPage() {
+ const newsletterRef = useRef(null)
+ const contactRef = useRef(null)
+
+ async function handleNewsletterSubmit(e: React.FormEvent) {
+ e.preventDefault()
+ const token = newsletterRef.current?.getResponse()
+
+ if (!token) {
+ alert('Please complete the CAPTCHA')
+ return
+ }
+
+ // Submit form...
+
+ // Reset only this widget
+ newsletterRef.current?.reset()
+ }
+
+ async function handleContactSubmit(e: React.FormEvent) {
+ e.preventDefault()
+ const token = contactRef.current?.getResponse()
+
+ if (!token) {
+ alert('Please complete the CAPTCHA')
+ return
+ }
+
+ // Submit form...
+
+ // Reset only this widget
+ contactRef.current?.reset()
+ }
+
+ return (
+ <>
+
+
Newsletter Signup
+
+
+
+
+
+
+
+
+
Contact Form
+
+
+
+
+
+
+
+ >
+ )
+}
+```
+
+## Manual Script Injection (Recommended for Multiple Widgets)
+
+When using multiple widgets, manual script injection ensures optimal loading:
+
+```tsx
+import {
+ Turnstile,
+ SCRIPT_URL,
+ DEFAULT_SCRIPT_ID
+} from '@marsidev/react-turnstile'
+import Script from 'next/script'
+
+export default function MultiFormPage() {
+ return (
+ <>
+ {/* Single script for all widgets */}
+
+
+
+