caputchin
All docs
View raw .md

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:

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:

// 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:

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.

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:

// 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:

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 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