@zaptcha/widget
v1.0.11
Published
[](https://www.npmjs.com/package/@zaptcha/widget) [](https://jsr.io/@zaptcha/widget) [
npm install @zaptcha/widgetyarn add @zaptcha/widgetpnpm add @zaptcha/widgetDeno / JSR
import ZeroCaptcha from "jsr:@zaptcha/widget";Browser (CDN)
<script type="module">
import ZeroCaptcha from "https://cdn.jsdelivr.net/npm/@zaptcha/widget@1/dist/zaptcha-widget.js";
</script>Quick Start
Basic HTML
<!DOCTYPE html>
<html>
<head>
<style>
:root {
--zaptcha-accent: #4f46e5;
--zaptcha-success: #10b981;
}
</style>
</head>
<body>
<form id="myForm">
<input type="text" placeholder="Your name" required />
<zero-captcha
base-url="https://api.example.com"
config-id="AAAA"
></zero-captcha>
<button type="submit">Submit</button>
</form>
<script type="module">
import ZeroCaptcha from '@zaptcha/widget';
const form = document.getElementById('myForm');
const captcha = form.querySelector('zero-captcha');
let token = null;
captcha.addEventListener('zaptcha-success', (e) => {
token = e.detail.token;
console.log('✓ CAPTCHA verified:', token);
});
captcha.addEventListener('zaptcha-fail', (e) => {
console.error('✗ CAPTCHA failed:', e.detail.reason);
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!token) {
alert('Please solve the CAPTCHA');
return;
}
// Send form with token to server
const formData = new FormData(form);
formData.append('captcha_token', token);
const response = await fetch('/api/submit', {
method: 'POST',
body: formData
});
if (response.ok) {
console.log('✓ Form submitted');
captcha.reset();
}
});
</script>
</body>
</html>React
import { useEffect, useRef, useState } from 'react';
import ZeroCaptcha from '@zaptcha/widget';
export function CaptchaForm() {
const captchaRef = useRef(null);
const [token, setToken] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const el = captchaRef.current;
if (!el) return;
const handleSuccess = (e) => setToken(e.detail.token);
const handleFail = (e) => console.error(e.detail.reason);
el.addEventListener('zaptcha-success', handleSuccess);
el.addEventListener('zaptcha-fail', handleFail);
return () => {
el.removeEventListener('zaptcha-success', handleSuccess);
el.removeEventListener('zaptcha-fail', handleFail);
};
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
if (!token) return alert('Solve CAPTCHA first');
setLoading(true);
const res = await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
});
setLoading(false);
if (res.ok) {
captchaRef.current?.reset();
setToken(null);
}
};
return (
<form onSubmit={handleSubmit}>
<input type="email" placeholder="Email" required />
<zero-captcha
ref={captchaRef}
base-url="https://api.example.com"
config-id="AAAA"
/>
<button type="submit" disabled={!token || loading}>
{loading ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}Vue 3
<template>
<form @submit.prevent="handleSubmit">
<input v-model="email" type="email" placeholder="Email" required />
<zero-captcha
ref="captchaRef"
base-url="https://api.example.com"
config-id="AAAA"
@zaptcha-success="onSuccess"
@zaptcha-fail="onFail"
/>
<button type="submit" :disabled="!token || loading">
{{ loading ? 'Submitting...' : 'Submit' }}
</button>
</form>
</template>
<script setup>
import { ref } from 'vue';
import ZeroCaptcha from '@zaptcha/widget';
const captchaRef = ref(null);
const email = ref('');
const token = ref(null);
const loading = ref(false);
const onSuccess = (e) => {
token.value = e.detail.token;
};
const onFail = (e) => {
console.error(e.detail.reason);
};
const handleSubmit = async () => {
if (!token.value) return;
loading.value = true;
const res = await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.value, token: token.value })
});
loading.value = false;
if (res.ok) {
captchaRef.value?.reset();
token.value = null;
}
};
</script>Angular
import { Component, ViewChild, ElementRef } from '@angular/core';
import { FormsModule } from '@angular/forms';
import ZeroCaptcha from '@zaptcha/widget';
@Component({
selector: 'app-captcha-form',
template: `
<form (ngSubmit)="onSubmit()">
<input
[(ngModel)]="email"
name="email"
type="email"
placeholder="Email"
required
/>
<zero-captcha
#captchaEl
base-url="https://api.example.com"
config-id="AAAA"
(zaptcha-success)="onSuccess($event)"
(zaptcha-fail)="onFail($event)"
/>
<button
type="submit"
[disabled]="!token || loading"
>
{{ loading ? 'Submitting...' : 'Submit' }}
</button>
</form>
`,
standalone: true,
imports: [FormsModule]
})
export class CaptchaFormComponent {
@ViewChild('captchaEl') captchaEl!: ElementRef<any>;
email = '';
token: string | null = null;
loading = false;
onSuccess(event: CustomEvent<{ token: string }>) {
this.token = event.detail.token;
}
onFail(event: CustomEvent<{ reason: string }>) {
console.error(event.detail.reason);
}
async onSubmit() {
if (!this.token) return;
this.loading = true;
const res = await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: this.email, token: this.token })
});
this.loading = false;
if (res.ok) {
this.captchaEl.nativeElement.reset();
this.token = null;
}
}
}Using the Core API (Headless)
For programmatic usage without the UI widget:
import { solve, ZaptchaCore } from '@zaptcha/widget/core';
// One-shot — simplest usage
const token = await solve('https://api.example.com', 'AAAA');
// Full lifecycle control
const core = new ZaptchaCore({
baseUrl: 'https://api.example.com',
configId: 'AAAA',
onSuccess: (token) => console.log('Token:', token),
onError: (reason) => console.error('Error:', reason),
onProgress: ({ pct }) => console.log(pct + '%'),
});
await core.warm(); // optional — pre-initialises worker pool
core.start();
// later:
core.destroy();Using mount() Helper
Cap.js-style imperative API:
import { mount } from '@zaptcha/widget';
const cap = mount(document.querySelector('zero-captcha'), {
onSuccess(token) { console.log('Token:', token); },
onError(reason) { console.error('Error:', reason); },
onProgress(data) { console.log('Progress:', data.pct + '%'); },
onReset() { console.log('Reset'); },
});
// Reset programmatically when needed
cap.reset();Styling
Customize appearance with CSS custom properties:
:root {
/* Light mode (defaults) */
--zaptcha-bg: #ffffff;
--zaptcha-border: #e5e7eb;
--zaptcha-border-hover: #d1d5db;
--zaptcha-border-focus: #4f46e5;
--zaptcha-text: #374151;
--zaptcha-text-muted: #9ca3af;
--zaptcha-accent: #4f46e5;
--zaptcha-success: #10b981;
--zaptcha-shadow: 0 2px 6px rgba(0,0,0,0.04);
--zaptcha-radius: 8px;
}
@media (prefers-color-scheme: dark) {
:root {
--zaptcha-bg: #1f2937;
--zaptcha-border: #374151;
--zaptcha-border-hover: #4b5563;
--zaptcha-border-focus: #818cf8;
--zaptcha-text: #f3f4f6;
--zaptcha-text-muted: #6b7280;
--zaptcha-accent: #818cf8;
--zaptcha-success: #34d399;
--zaptcha-shadow: 0 2px 6px rgba(0,0,0,0.3);
}
}
zero-captcha {
width: 100%;
max-width: 210px;
}API Reference
Attributes
| Name | Type | Required | Description |
|------|------|----------|-------------|
| base-url | string | ✓ | Backend API base URL |
| config-id | string | ✓ | Base64url-encoded config ID |
| worker-url | string | | Optional external worker file path (bypasses built-in WASM worker) |
Events
| Event | Detail | Description |
|-------|--------|-------------|
| zaptcha-success | { token: string } | CAPTCHA solved, token generated |
| zaptcha-fail | { reason: string } | Solve failed |
| zaptcha-reset | {} | Widget reset to idle state |
| zaptcha-progress | { index, nonce, total, pct } | Solving progress update |
Methods
element.reset(): void
// Reset widget to idle stateCallback Properties (cap.js-style)
element.onSuccess = (token) => {}
element.onError = (reason) => {}
element.onProgress = ({ index, nonce, total, pct }) => {}
element.onReset = () => {}Backend Integration
Your backend needs two endpoints:
GET /{config-id}/challenge
Returns:
{
"token": "eyJ...",
"challenge_count": 16,
"salt_length": 32,
"difficulty": 20
}POST /{config-id}/redeem
Request body:
{
"token": "eyJ...",
"solutions": [12345, 67890, ...]
}Response:
{
"message": "proof_token_xyz"
}Browser Support
| Browser | Version | |---------|---------| | Chrome | 76+ | | Firefox | 79+ | | Safari | 14.1+ | | Edge | 79+ |
WebAssembly and ES2020 modules required.
Troubleshooting
WASM not loading?
- Ensure WASM is served with
Content-Type: application/wasm - Check browser console for any WASM instantiation errors
Events not firing?
- Use
addEventListener()(noton*attributes for Custom Events) - Event names are
zaptcha-success, notzaptchaSuccess
Styling not applying?
- CSS custom properties must be set on a parent element or
:root - The widget uses Shadow DOM, so styles must be applied via CSS custom properties
Worker issues?
- If using a custom
worker-url, ensure the path is correct and the file is accessible - The worker file must be served from the same origin or with proper CORS headers
Performance
- Challenge solving: 100-500ms (varies by difficulty and device)
- Parallel WASM workers: up to 8 workers (based on hardware concurrency)
- Component size: minimal footprint with bundled WASM
Security
- No cookies stored
- No tracking pixels
- No external requests (except to your API)
- Proof-of-work verified on server
- Token valid for single use only
License
MIT © 2024 Zaptcha
Contributing
Contributions are welcome! Please open an issue or submit a PR.
