<!-- Markdown version of the Capcat doc "Frontend Widget" (rendered page: https://capcat.ai/guide/widget.html). Docs index: https://capcat.ai/llms.txt, full single-file version: https://capcat.ai/llms-full.txt -->

# Frontend Widget

The widget is a standard Web Component (`<cap-widget>`), loaded with a single `<script>` tag and usable with any framework.

## Setup

```html
<script src="https://capcat.ai/widget/cap.js"></script>
```

> **Tip**
>
> This script is hosted by Capcat itself on capcat.ai, with no third-party CDN dependency, so it loads reliably worldwide (including China).

## Form mode (zero JavaScript)

When the widget is placed inside a `<form>`, it automatically injects a hidden `cap-token` field and submits it along with the form:

```html
<form action="/submit" method="POST">
  <cap-widget data-cap-api-endpoint="https://api.capcat.ai/<site-key>/"></cap-widget>
  <button type="submit">Submit</button>
</form>
```

## JavaScript mode (SPA / custom flow)

Listen for the `solve` event to get the token and decide yourself when to send it:

```js
const widget = document.querySelector("cap-widget");
widget.addEventListener("solve", (e) => {
  const token = e.detail.token;
  // send the token to your backend, unlock the submit button, etc.
});
```

## Common attributes

| Attribute | Description |
| --- | --- |
| `data-cap-api-endpoint` | The verification service URL, in the form `https://api.capcat.ai/<site-key>/` (the site key is generated when you create a site in the [console](https://capcat.ai/guide/console.md)). Required. |
| `data-cap-theme` | Set to `auto` to make the widget match your page's existing styles — see [Auto theme](#auto-theme). |
| `data-cap-branding` | Set to `hidden` to remove the Capcat badge in the widget's corner — see [Hiding the badge](#hiding-the-badge-paid-plans). |
| `data-cap-worker-count` | Number of Web Workers used for solving, defaults to the device's core count. |

## Styling

The widget renders inside Shadow DOM, so your page's CSS can't accidentally break it. Customization goes through two supported channels — auto theme and `--cap-*` CSS variables — which can be freely combined.

### Auto theme

Add `data-cap-theme="auto"` and the widget derives its look from the page around it, no CSS required:

```html
<cap-widget data-cap-theme="auto" data-cap-api-endpoint="..."></cap-widget>
```

What gets sampled:

- **Font family, size & text color** — inherited from the widget's position in the page; font size follows the reference input.
- **Background** — the first opaque ancestor background, so it matches the card or panel it sits on and adapts to dark pages automatically.
- **Border color & radius** — aligned with the first text input in the same `<form>`. Outside a form, the nearest surrounding form control is used, falling back to the nearest button (radius only, capped at `24px`). The checkbox radius scales proportionally.
- **Accent** — the form's `accent-color` if set, otherwise the text color (used for the spinner, focus ring and troubleshooting link).

Auto theme only fills in variables you haven't set yourself: any `--cap-*` variable declared in your CSS takes precedence, so you can combine `auto` with the manual overrides below. It re-samples when the OS color scheme flips; if your app switches themes at runtime (e.g. a class toggle), re-set the attribute (`el.setAttribute("data-cap-theme", "auto")`) after switching to re-sample.

### CSS variables

Set any of these on the `cap-widget` element (values shown are the defaults):

```css
cap-widget {
  --cap-background: #fdfdfd;
  --cap-border-color: #dddddd8f;
  --cap-border-radius: 14px;
  --cap-widget-width: 260px;
  --cap-widget-height: 58px;
  --cap-widget-padding: 14px;
  --cap-gap: 15px;
  --cap-color: #212121;
  --cap-font: system-ui, sans-serif;
  --cap-font-size: 15px;
  --cap-checkbox-size: 25px;
  --cap-checkbox-border: 1px solid #aaaaaad1;
  --cap-checkbox-border-radius: 6px;
  --cap-checkbox-background: #fafafa91;
  --cap-spinner-color: #000;
  --cap-spinner-background-color: #eee;
  --cap-focus-ring: #0066cc;
}
```

A common recipe — make the widget fill its container like other form fields:

```css
cap-widget {
  display: block;
  --cap-widget-width: 100%;
}
```

### Hiding the badge (paid plans)

`data-cap-branding="hidden"` removes the Capcat badge in the widget's corner. This is a paid-plan feature: enable **Hide the Capcat badge** for the site in the [console](https://capcat.ai/guide/console.md) first — on free sites the server makes the badge reappear.

## Content Security Policy

If your pages send a `Content-Security-Policy`, the widget needs these allowances. Everything is served from `capcat.ai` / `api.capcat.ai`; no third-party CDN is contacted.

| Directive | Value | Why |
| --- | --- | --- |
| `script-src` | `https://capcat.ai` `'unsafe-eval'` `'wasm-unsafe-eval'`, plus `'unsafe-inline'` **or** a nonce | `cap.js` itself, the pako fallback script, compiling the solver WebAssembly module, and the instrumentation check that runs inside a sandboxed `srcdoc` iframe. That iframe inherits your page's CSP: its script is inline (hence `'unsafe-inline'` or a nonce) and its environment checks use `new Function` / `eval` (hence `'unsafe-eval'`). Without them the check fails silently and the widget reports an error after a 20 s timeout |
| `connect-src` | `https://api.capcat.ai https://capcat.ai` | Challenge/redeem API calls, and fetching `cap_wasm_bg.*.wasm` |
| `worker-src` | `blob:` | Solver workers are created from a Blob URL (use `child-src` for browsers without `worker-src`) |
| `style-src` | `'unsafe-inline'` **or** a nonce | The widget injects one `<style>` into its shadow root |

A minimal policy that keeps everything else locked down:

```
Content-Security-Policy: default-src 'self';
  script-src 'self' https://capcat.ai 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval';
  connect-src 'self' https://api.capcat.ai https://capcat.ai;
  worker-src blob:;
  style-src 'self' 'unsafe-inline'
```

### Nonces instead of `'unsafe-inline'`

Set these globals **before** `cap.js` loads and the widget stamps your nonce onto everything it injects:

- `window.CAP_CSS_NONCE` — the shadow-root `<style>` (lets you drop `'unsafe-inline'` from `style-src`).
- `window.CAP_SCRIPT_NONCE` — the inline script inside the instrumentation iframe and any provider script tags the widget adds (Turnstile / reCAPTCHA in Account Defense setups). Falls back to `CAP_CSS_NONCE` when unset. Keep `'unsafe-eval'` in `script-src` either way; a nonce only replaces `'unsafe-inline'`.

```html
<script nonce="r4nd0m">
  window.CAP_CSS_NONCE = "r4nd0m";
  window.CAP_SCRIPT_NONCE = "r4nd0m";
</script>
<script src="https://capcat.ai/widget/cap.js" nonce="r4nd0m" async defer></script>
```

### Hosting assets yourself

If your policy cannot allow `capcat.ai` for a given directive, point the widget at copies on your own origin and adjust the directives above accordingly:

- `window.CAP_CUSTOM_WASM_URL` — absolute URL of `cap_wasm_bg.wasm` (download it from `https://capcat.ai/widget/cap_wasm_bg.wasm`).
- `window.CAP_PAKO_URL` — URL of `pako_inflate.min.js`. The fallback is only loaded on browsers without `DecompressionStream`; the default copy is pinned with Subresource Integrity, a custom URL is loaded as-is.

Assign these globals from a script as plain strings. The widget ignores any other kind of value — including page elements that happen to share the name, such as `<a id="CAP_CSS_NONCE">` in user-generated content — and only accepts asset URLs that are `https:` or on your page's own origin.

For React / Vue / Svelte integration examples, see the upstream [Widget docs](https://trycap.dev/guide/widget.html).
