---
type: how-to
title: Use Caputchin with React (and Next.js)
description: Mount the widget custom element in a React tree, handle events with refs, and verify on the server.
---

# Use Caputchin with React

The Caputchin widget is a standard web component (`<caputchin-widget>`), so React renders it the same way as any native element. There is no React-specific wrapper to install. The notes below cover the bits that are different from server-rendered HTML.

## Mount the widget

Add the widget script once in your `<head>` (or use `next/script` for Next.js apps), then drop the custom element into JSX:

```tsx
import Script from "next/script";

export default function SignupForm() {
  return (
    <>
      <Script
        src="https://cdn.jsdelivr.net/npm/@caputchin/widget@1/dist/widget.js"
        strategy="afterInteractive"
      />
      <form action="/api/signup" method="POST">
        <input name="email" type="email" required />
        <caputchin-widget sitekey="cpt_pub_..." />
        <button type="submit">Sign up</button>
      </form>
    </>
  );
}
```

In a non-Next app, replace `<Script>` with a plain `<script src="..." defer></script>` in `index.html`.

## TypeScript JSX types

React's JSX doesn't know about `<caputchin-widget>` out of the box. Add an ambient declaration once:

```ts
// src/caputchin-widget.d.ts
import type { DetailedHTMLProps, HTMLAttributes } from "react";

declare module "react" {
  namespace JSX {
    interface IntrinsicElements {
      "caputchin-widget": DetailedHTMLProps<
        HTMLAttributes<HTMLElement> & {
          sitekey: string;
          game?: string;
          games?: string;
          "game-src"?: string;
          mode?: "auto" | "form-submit" | "manual";
        },
        HTMLElement
      >;
    }
  }
}
```

## Listen for events

Web component events do not flow through React's synthetic event system. Use a ref + `addEventListener` in a `useEffect`:

```tsx
import { useEffect, useRef } from "react";

export function VerifiedForm() {
  const ref = useRef<HTMLElement>(null);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const onComplete = (e: Event) => {
      const detail = (e as CustomEvent).detail;
      console.log("verified", detail.token);
    };
    el.addEventListener("complete", onComplete);
    return () => el.removeEventListener("complete", onComplete);
  }, []);

  return <caputchin-widget ref={ref} sitekey="cpt_pub_..." />;
}
```

The full event list is in [widget](../widget.md#events).

## Server-side rendering

`<caputchin-widget>` is a custom element — it renders as an empty tag during SSR and upgrades on the client when the widget script loads. No special handling is needed beyond making sure the script tag is present.

If you're using React Server Components, the form action handler (a server function) can verify the token directly:

```tsx
// app/signup/actions.ts
"use server";

export async function signup(formData: FormData) {
  const token = formData.get("caputchin-token");
  const verdict = await fetch("https://api.caputchin.com/api/v1/siteverify", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      secret: process.env.CAPUTCHIN_SECRET,
      response: token,
    }),
  }).then((r) => r.json());

  if (!verdict.success) {
    return { ok: false, errors: verdict["error-codes"] };
  }
  // Continue your sign-up flow.
}
```

## Programmatic mode (manual)

For SPA flows where the form does not auto-submit, `mode="manual"` gives you direct control:

```tsx
const ref = useRef<HTMLElement>(null);

function onClick() {
  // @ts-expect-error widget exposes .start() once upgraded
  ref.current?.start();
}

return (
  <>
    <caputchin-widget ref={ref} sitekey="cpt_pub_..." mode="manual" />
    <button onClick={onClick}>Verify me</button>
  </>
);
```

`start()` returns `void`; observe progress via the `start` / `complete` / `error` events. See [widget — programmatic API](../widget.md#programmatic-api-manual-mode) for the full method list.

## Common pitfalls

| Symptom | Cause | Fix |
|---|---|---|
| Widget never appears | Script loaded after the React tree hydrated and the custom element didn't upgrade | Use `next/script strategy="afterInteractive"` or move the script tag above the React root |
| TypeScript error on `<caputchin-widget>` | Missing JSX intrinsic declaration | Add the `.d.ts` shown above |
| `complete` event handler never fires | Used `onComplete={...}` instead of `addEventListener` | React props for unknown lowercase tags are passed as HTML attributes, not event listeners. Use a ref + `addEventListener` |
| Form submits before token is injected | Default `mode="auto"` does not gate submission | Set `mode="form-submit"` on the widget so the form waits for the widget to complete |
