2026-03-02 17:10:54 +00:00
---
outline: [2, 3, 4]
---
2025-01-18 13:46:25 +00:00
2026-03-02 17:10:54 +00:00
# Widget
2025-12-21 13:28:54 +00:00
2026-03-02 20:06:39 +00:00
Cap's client-side widget handles requesting, solving and displaying challenges using a native web component and rust-flavoured WASM. It also includes the [programmatic mode ](./programmatic ).
2026-03-02 17:10:54 +00:00
## Installation
2025-01-18 13:46:25 +00:00
2025-04-22 14:41:19 +01:00
::: code-group
2026-03-02 17:10:54 +00:00
```sh [pnpm]
pnpm add @cap.js/widget
2025-01-18 13:46:25 +00:00
` ``
2026-03-02 17:10:54 +00:00
` ``sh [npm]
npm i @cap.js/widget
2025-04-22 14:41:19 +01:00
` ``
2025-05-07 19:30:12 +01:00
2026-03-02 17:10:54 +00:00
` ``sh [bun]
bun add @cap.js/widget
2025-09-23 17:18:31 +01:00
` ``
2026-03-02 17:10:54 +00:00
` ``html [cdn]
<!--
* you should pin a specific version in production to avoid breaking changes. alternatively, you can also use the standalone asset server
* ` cdn.jsdelivr.net` is blocked in some jurisdictions, like some parts of China. if your website needs to be reachable from these jurisdictions, we recommend that you use npm.
-->
<script type="module" src="https://cdn.jsdelivr.net/npm/@cap.js/widget"></script>
2025-09-23 17:26:56 +01:00
` ``
2026-02-28 18:17:07 +00:00
2025-05-21 16:36:02 +01:00
:::
2026-03-02 17:10:54 +00:00
## Usage
2025-01-18 13:46:25 +00:00
2026-03-02 17:10:54 +00:00
The widget requires a ` data-cap-api-endpoint` pointing at your Cap deployment. For Standalone instances, this is:
` ``
https://<your-instance>/<site-key>/
2025-01-18 13:46:25 +00:00
` ``
2026-03-02 17:10:54 +00:00
### Vanilla
2025-01-18 13:46:25 +00:00
2026-03-02 17:10:54 +00:00
` ``html
<form>
<cap-widget id="cap" data-cap-api-endpoint="https://<your-instance>/<site-key>/"></cap-widget>
<button type="submit">Submit</button>
</form>
2025-09-23 17:26:56 +01:00
2026-03-02 17:10:54 +00:00
<script type="module">
import "https://cdn.jsdelivr.net/npm/@cap.js/widget";
2025-01-18 13:46:25 +00:00
2026-03-02 17:10:54 +00:00
document.getElementById("cap").addEventListener("solve", (e) => {
console.log("token:", e.detail.token);
});
</script>
` ``
::: tip
When the widget lives inside a ` <form>`, it automatically injects a hidden ` cap-token` input, and the token is submitted alongside your other fields with no extra JavaScript needed.
:::
### React
` ``jsx
import "@cap.js/widget";
export default function ContactForm() {
return (
<form>
<cap-widget
data-cap-api-endpoint="https://<your-instance>/<site-key>/"
onsolve={(e) => console.log("token:", e.detail.token)}
onprogress={(e) => console.log(e.detail.progress)}
onerror={(e) => console.error(e.detail.message)}
/>
<button type="submit">Submit</button>
</form>
);
}
` ``
::: tip
We recommend using React 19 or later as it improves custom element event handling
:::
### Vue
` ``vue
<script setup>
import "@cap.js/widget";
</script>
<template>
<form>
<cap-widget
data-cap-api-endpoint="https://<your-instance>/<site-key>/"
@solve="(e) => console.log('token:', e.detail.token)"
@progress="(e) => console.log(e.detail.progress)"
@error="(e) => console.error(e.detail.message)"
/>
<button type="submit">Submit</button>
</form>
</template>
` ``
If you're getting an unknown-component warning, add this to your ` vite.config.js`:
2025-01-18 13:46:25 +00:00
2025-09-23 17:26:56 +01:00
` ``js
2026-03-02 17:10:54 +00:00
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: { isCustomElement: (tag) => tag.startsWith("cap-") },
},
}),
],
2025-01-18 13:46:25 +00:00
});
` ``
2026-03-02 17:10:54 +00:00
### Svelte 5
` ``svelte
<script>
import "@cap.js/widget";
</script>
<form>
<cap-widget
data-cap-api-endpoint="https://<your-instance>/<site-key>/"
on:solve={(e) => console.log("token:", e.detail.token)}
on:progress={(e) => console.log(e.detail.progress)}
on:error={(e) => console.error(e.detail.message)}
/>
<button type="submit">Submit</button>
</form>
` ``
### SolidJS
` ``jsx
import "@cap.js/widget";
export default function ContactForm() {
return (
<form>
<cap-widget
data-cap-api-endpoint="https://<your-instance>/<site-key>/"
on:solve={(e) => console.log("token:", e.detail.token)}
on:progress={(e) => console.log(e.detail.progress)}
on:error={(e) => console.error(e.detail.message)}
/>
<button type="submit">Submit</button>
</form>
);
}
` ``
### Astro
` ``astro
---
// ContactForm.astro
---
<form>
<cap-widget id="cap" data-cap-api-endpoint="https://<your-instance>/<site-key>/" />
<button type="submit">Submit</button>
</form>
<script>
import "@cap.js/widget";
document.getElementById("cap").addEventListener("solve", (e) => {
console.log("token:", e.detail.token);
});
</script>
` ``
If you're rendering a React/Vue/Svelte component inside Astro, follow that framework's guide above and add ` client:load` to the component.
### Preact
` ``jsx
import "@cap.js/widget";
export default function ContactForm() {
return (
<form>
<cap-widget
data-cap-api-endpoint="https://<your-instance>/<site-key>/"
onsolve={(e) => console.log("token:", e.detail.token)}
onprogress={(e) => console.log(e.detail.progress)}
onerror={(e) => console.error(e.detail.message)}
/>
<button type="submit">Submit</button>
</form>
);
}
` ``
### Qwik
` ``tsx
import { component$ } from "@builder.io/qwik";
import "@cap.js/widget";
export default component$(() => {
return (
<form>
<cap-widget
data-cap-api-endpoint="https://<your-instance>/<site-key>/"
on:solve$={(e: CustomEvent) => console.log("token:", e.detail.token)}
on:progress$={(e: CustomEvent) => console.log(e.detail.progress)}
on:error$={(e: CustomEvent) => console.error(e.detail.message)}
/>
<button type="submit">Submit</button>
</form>
);
});
` ``
2025-01-18 13:46:25 +00:00
2026-03-02 20:06:39 +00:00
## Programmatic mode
2026-02-28 18:17:07 +00:00
2026-03-02 20:06:39 +00:00
If you don't want a visible widget, for example when protecting a background action like a post submission, use the [programmatic mode](./programmatic):
2025-09-23 17:18:31 +01:00
` ``js
2026-02-28 18:17:07 +00:00
import Cap from "@cap.js/widget";
2025-09-23 17:18:31 +01:00
const cap = new Cap({
2026-03-02 17:10:54 +00:00
apiEndpoint: "https://<your-instance>/<site-key>/",
2025-09-23 17:18:31 +01:00
});
2026-03-02 17:10:54 +00:00
const { token } = await cap.solve();
2025-09-23 17:18:31 +01:00
` ``
2026-03-02 17:10:54 +00:00
## Events
2025-04-12 13:39:56 +01:00
2026-03-02 17:10:54 +00:00
All events are dispatched as CustomEvent.
2025-01-18 13:46:25 +00:00
2026-03-02 17:10:54 +00:00
| Event | When it fires | Detail |
| ---------- | ------------------------------ | ---------------------- |
| ` solve` | Challenge solved successfully | ` { token: string }` |
| ` progress` | Progress update during solving | ` { progress: number }` |
| ` error` | An error occurred | ` { message: string }` |
| ` reset` | Widget reset to initial state | ` {}` |
2025-04-22 13:42:58 +01:00
2026-03-02 17:10:54 +00:00
## Options
2025-05-07 19:30:12 +01:00
2026-03-02 17:10:54 +00:00
You can also specify a custom fetch function with ` window.CAP_CUSTOM_FETCH`:
` ``js
window.CAP_CUSTOM_FETCH = (url, params) => fetch(url, params);
` ``
You can also set a custom WASM url (for example the Standalone asset server's) with ` window.CAP_CUSTOM_WASM_URL` or set a nonce for the CSS with ` window.CAP_CSS_NONCE`
### Attributes
| Attribute | Description |
| ------------------------------ | ----------------------------------------------------------------------------- |
| ` data-cap-api-endpoint` | **Required.** Your Cap endpoint: ` https://<instance>/<site-key>/` |
| ` data-cap-worker-count` | Number of solver workers (defaults to ` navigator.hardwareConcurrency \|\| 8`) |
| ` data-cap-hidden-field-name` | Name of the hidden token input in a ` <form>` (default: ` cap-token`) |
| ` data-cap-troubleshooting-url` | Custom URL for the "Troubleshooting" link shown when a user is blocked |
#### i18n
All widget labels can be overridden with ` data-cap-i18n-*` attributes. These default to English
2025-05-07 19:30:12 +01:00
` ``html
<cap-widget
2026-03-02 17:10:54 +00:00
data-cap-api-endpoint="https://<your-instance>/<site-key>/"
data-cap-i18n-initial-state="Verify you're human"
2025-05-07 19:30:12 +01:00
data-cap-i18n-verifying-label="Verifying..."
2026-03-02 17:10:54 +00:00
data-cap-i18n-solved-label="You're human"
2025-05-07 19:30:12 +01:00
data-cap-i18n-error-label="Error"
2026-02-28 18:17:07 +00:00
data-cap-i18n-troubleshooting-label="Troubleshooting"
2025-09-08 16:34:50 +01:00
data-cap-i18n-wasm-disabled="Enable WASM for significantly faster solving"
2026-03-02 17:10:54 +00:00
data-cap-i18n-verify-aria-label="Click to verify you're a human"
data-cap-i18n-verifying-aria-label="Verifying, please wait"
data-cap-i18n-verified-aria-label="Verified"
data-cap-i18n-error-aria-label="An error occurred, please try again"
2025-05-07 19:30:12 +01:00
></cap-widget>
` ``
2026-03-02 17:10:54 +00:00
### Styling
2025-06-16 14:18:28 +01:00
2026-03-02 17:10:54 +00:00
Override any of these CSS variables on the ` cap-widget` element:
2025-06-13 14:53:42 +01:00
` ``css
cap-widget {
--cap-background: #fdfdfd;
--cap-border-color: #dddddd8f;
--cap-border-radius: 14px;
--cap-widget-height: 30px;
--cap-widget-width: 230px;
--cap-widget-padding: 14px;
--cap-gap: 15px;
--cap-color: #212121;
--cap-checkbox-size: 25px;
--cap-checkbox-border: 1px solid #aaaaaad1;
--cap-checkbox-border-radius: 6px;
--cap-checkbox-background: #fafafa91;
--cap-checkbox-margin: 2px;
2026-03-02 17:10:54 +00:00
--cap-font: system-ui, -apple-system, sans-serif;
2025-06-13 14:53:42 +01:00
--cap-spinner-color: #000;
--cap-spinner-background-color: #eee;
--cap-spinner-thickness: 5px;
}
2025-06-16 14:18:28 +01:00
` ``