---
type: how-to
title: Frontend integration examples
summary: Mount the widget in plain HTML, React, Vue, Svelte, Angular, and Next.js. The element is the same everywhere.
---

The widget is a standard custom element, so it works in every framework with no wrapper to install. The shape is always the same: load the package once ([client side integration](/docs/integration-guides/cdn)), render `<caputchin-widget sitekey="cpt_pub_...">` inside your form, and read the hidden `caputchin-token` field it injects when the visitor clears the challenge. For a game, swap in `<caputchin-game ... game="caputchin/games/leaf-memory">`.

<Callout type="warn">
If a site key has the game gate turned on (its **Require a game to verify** setting), the checkbox `<caputchin-widget>` cannot pass on that key: the server requires a game, so the widget raises a `gate-unavailable` error. Use `<caputchin-game>` on any gated key. This is by design, because the gate is enforced on Caputchin's side and a checkbox cannot satisfy it. See [the game gate](/docs/site-keys/games).
</Callout>

The examples below differ only in each framework's syntax and its one custom-element quirk.

## Plain HTML

The injected field rides along with a normal form POST, so there is nothing to wire up:

```html
<script src="https://cdn.jsdelivr.net/npm/@caputchin/widget@3/dist/widget.js"></script>

<form action="/contact" method="POST">
  <input name="email" type="email" required />
  <caputchin-widget sitekey="cpt_pub_..."></caputchin-widget>
  <button type="submit">Send</button>
</form>
```

## React

Import once at startup, then read the field from `FormData` on submit:

```jsx
import "@caputchin/widget";

export function ContactForm() {
  async function handleSubmit(e) {
    e.preventDefault();
    const data = new FormData(e.currentTarget);
    await fetch("/api/contact", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email: data.get("email"), token: data.get("caputchin-token") }),
    });
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <caputchin-widget sitekey="cpt_pub_..." />
      <button type="submit">Send</button>
    </form>
  );
}
```

If you use TypeScript with React, declare both elements once so JSX accepts them:

```ts
// caputchin.d.ts
import type { DetailedHTMLProps, HTMLAttributes } from "react";
declare module "react" {
  namespace JSX {
    interface IntrinsicElements {
      "caputchin-widget": DetailedHTMLProps<HTMLAttributes<HTMLElement> & { sitekey: string }, HTMLElement>;
      "caputchin-game": DetailedHTMLProps<HTMLAttributes<HTMLElement> & { sitekey: string; game?: string; games?: string }, HTMLElement>;
    }
  }
}
```

This `declare module "react"` form is specific to React's JSX. In plain TypeScript the elements are just `HTMLElement` and need no declaration, and the other frameworks below type custom elements their own way, so you only need this block in a React project.

## Vue

Tell Vue's compiler that `caputchin-` tags are custom elements, so it does not try to resolve them as components:

```js
// vite.config.js
export default {
  plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith("caputchin-") } } })],
};
```

```vue
<script setup>
import "@caputchin/widget";
function onSubmit(e) {
  const data = new FormData(e.target);
  // send data.get("caputchin-token") to your backend
}
</script>

<template>
  <form @submit.prevent="onSubmit">
    <input name="email" type="email" required />
    <caputchin-widget sitekey="cpt_pub_..."></caputchin-widget>
    <button type="submit">Send</button>
  </form>
</template>
```

## Svelte

Svelte renders custom elements as-is, no config needed:

```svelte
<script>
  import "@caputchin/widget";
  function onSubmit(e) {
    const data = new FormData(e.currentTarget);
    // send data.get("caputchin-token") to your backend
  }
</script>

<form on:submit|preventDefault={onSubmit}>
  <input name="email" type="email" required />
  <caputchin-widget sitekey="cpt_pub_..."></caputchin-widget>
  <button type="submit">Send</button>
</form>
```

## Angular

Add `CUSTOM_ELEMENTS_SCHEMA` to the module or standalone component so Angular allows the unknown tag, and import the package once (in `main.ts`):

```ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import "@caputchin/widget";

@Component({
  selector: "contact-form",
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <form (submit)="onSubmit($event)">
      <input name="email" type="email" required />
      <caputchin-widget sitekey="cpt_pub_..."></caputchin-widget>
      <button type="submit">Send</button>
    </form>
  `,
})
export class ContactFormComponent {
  onSubmit(e: Event) {
    const data = new FormData(e.target as HTMLFormElement);
    // send data.get("caputchin-token") to your backend
  }
}
```

## Next.js

The same React code works. The custom element renders as an empty tag during server rendering and upgrades on the client once the package loads, so no special handling is needed beyond importing `@caputchin/widget` in a client component (or loading the CDN script with `next/script`).

## See also

- [Client side integration](/docs/integration-guides/cdn) for the loading options.
- [Server side integration](/docs/integration-guides/verify-on-your-backend) for the backend half.
