Docs Widget

Widget

captcha.js is a zero-dependency script that handles the full PoW flow and shows a visible protection status inside your form.

You do not need to manage the Proof-of-Work flow yourself. Include captcha.js, add data-captcha to your form, and verify the result on your backend.

Overview

How the widget works

No checkbox, no puzzle image. The widget runs in the background and shows a small status panel so users know their form is protected.

  1. 1

    Scan on load, trigger on interaction

    The widget finds all [data-captcha] forms on load. When you first interact with a form (click, tap, or keystroke), the PoW challenge is fetched in the background and the status panel shows Preparing protection….

  2. 2

    Fetch and solve

    The widget calls /challenge then runs the PoW solver in a background Worker thread. The status panel shows Verifying form protection… during this phase.

  3. 3

    Ready

    Once solved, the status panel shows Protection active. The form is ready to submit — no action needed from the user.

  4. 4

    Append and submit

    On submit, the widget appends two hidden fields — captcha_token and captcha_solution — then re-submits the form.

Fields appended to your form

html
<input type="hidden" name="captcha_token"    value="a8f3c1e2b7d94f6a...">
<input type="hidden" name="captcha_solution" value="83421">

Forward these two values to /verify from your server.

Backend verification is required. The widget only handles the client-side PoW flow. You must call /verify server-side and reject the request if success is not true. Skipping this step leaves your form unprotected. See Backend Validation for examples in Laravel, Node.js, Python, and Go.

Inline widget

Visible inline status panel

The widget automatically renders a small status box inside your form. It honestly reflects what is happening in the background — no fake interaction required.

No interaction needed

Users never need to click. The panel is informational only.

Honest status

States reflect what the widget is actually doing — not a simulation.

Auto-injected

If no data-captcha-status container exists, the widget injects one automatically before the submit button.

States

State Message shown Trigger
idle Preparing protection… Challenge fetch started
solving Verifying form protection… PoW Worker running
ready Protection active Solution found, form ready
error Verification unavailable Missing site key or non-429 error from /challenge
rate_limited Please try again in X seconds /challenge returned 429

Placement

Add a data-captcha-status element anywhere inside your form to control where the panel appears. If omitted, the widget injects one automatically just before the submit button.

html
<form method="POST" action="/login" data-captcha>
    <input type="email"    name="email">
    <input type="password" name="password">

    <!-- optional: place and style however you like -->
    <div data-captcha-status></div>

    <button type="submit">Login</button>
</form>

Styling

When you provide your own data-captcha-status container, the widget keeps styling changes minimal so layout, size, and positioning can be controlled with your own CSS. Apply any additional styling directly on the element.

The widget also sets a data-captcha-state attribute on the container as each state changes, so you can hook into states with plain CSS:

css
[data-captcha-status] { font-size: 13px; padding: 8px 12px; }

[data-captcha-status][data-captcha-state="idle"],
[data-captcha-status][data-captcha-state="solving"] { color: #6b7280; }

[data-captcha-status][data-captcha-state="ready"]        { color: #059669; }
[data-captcha-status][data-captcha-state="error"]        { color: #dc2626; }
[data-captcha-status][data-captcha-state="rate_limited"] { color: #d97706; }

Or as inline styles directly on the element:

html
<div data-captcha-status style="width: 100%; border: 1px solid #e5e7eb; border-radius: 8px; padding: 8px 12px; text-align: center;"></div>

If no container is provided, the auto-injected element gets a small default presentation. You can still target it with [data-captcha-status][data-captcha-auto] in CSS.

Custom messages & translations

Set window.CAPTCHA_MESSAGES before loading the script to override any state text. You can override only the messages you need — any missing keys fall back to the built-in English defaults.

html
<script>
    window.CAPTCHA_BASE_URL = "https://captchaapi.eu/api/v1";
    window.CAPTCHA_SITE_KEY = "your-site-key";
    window.CAPTCHA_MESSAGES = {
        idle:         "Schutz wird vorbereitet…",
        solving:      "Formularschutz wird überprüft…",
        ready:        "Schutz aktiv",
        error:        "Überprüfung nicht verfügbar",
        rate_limited: "Bitte in {seconds} Sekunden erneut versuchen",
    };
</script>
<script src="https://captchaapi.eu/captcha.js" defer></script>

Each message is defined as a string. The rate_limited message also supports a {seconds} placeholder, or a function for full control.

javascript
window.CAPTCHA_MESSAGES = {
    rate_limited: "Please try again in {seconds} seconds",
};

Or provide a function for full control (useful for localization). For example, in Czech:

javascript
window.CAPTCHA_MESSAGES = {
    rate_limited: (seconds) => `Too many requests. Retry in ${seconds}s.`,
};
javascript
window.CAPTCHA_MESSAGES = {
    rate_limited: (seconds) => {
        if (seconds === 1) return `Zkuste to znovu za ${seconds} sekundu`;
        if (seconds >= 2 && seconds <= 4) return `Zkuste to znovu za ${seconds} sekundy`;
        return `Zkuste to znovu za ${seconds} sekund`;
    },
};

This is especially helpful for languages that need different wording depending on the retry value.

Key Default text Notes
idle Preparing protection…
solving Verifying form protection…
ready Protection active
error Verification unavailable
rate_limited Please try again in {seconds} seconds Supports {seconds} placeholder or a function
Under the hood

How Proof-of-Work works

PoW requires the client to do a small amount of computation before submitting. Bots struggle to do this at scale; real users never notice.

Challenge

The server issues a unique token containing a random nonce and the required difficulty.

Solve

The client iterates numbers until it finds one that, when hashed with the nonce, produces the required leading zeros.

Verify

The server re-hashes the token + solution. If the result has the required leading zeros, the challenge passes.

No tracking, no cookies. The widget makes no fingerprinting calls. It requests a challenge using your site_key, then submits the resulting token and solution through your backend verification flow.

Configuration

Difficulty

Difficulty controls how strict the Proof-of-Work requirement is. Higher levels require more attempts on average and can increase solve time significantly depending on device and browser.

Level Profile Work Typical time
1
Low Fast < 10 ms
2
Moderate Very fast ± 10–50 ms
3 default
Default Balanced tens to hundreds of ms
4
High Heavy hundreds of ms to 2+ s

Level 3 is the default and fits most use cases. Level 4 should be used carefully for high-value forms, especially if mobile and Safari performance matter.

Example

Login form example

A complete login form with a styled data-captcha-status panel. The panel background and text colour are updated via a MutationObserver watching data-captcha-state.

Login form showing the Protection active status panel
html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen bg-gray-50 flex items-center justify-center p-4">

    <div class="w-full max-w-sm">
        <div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
            <div class="mb-8">
                <h1 class="text-xl font-semibold text-gray-900">Sign in</h1>
                <p class="text-sm text-gray-500 mt-1">Welcome back. Enter your details below.</p>
            </div>

            <form method="POST" action="/login" data-captcha class="space-y-4">
                <div>
                    <label class="block text-sm font-medium text-gray-700 mb-1.5">Email</label>
                    <input type="email" name="email" placeholder="you@example.com"
                        class="w-full px-3.5 py-2.5 rounded-lg border border-gray-300 text-sm">
                </div>
                <div>
                    <div class="flex items-center justify-between mb-1.5">
                        <label class="block text-sm font-medium text-gray-700">Password</label>
                        <a href="#" class="text-xs text-gray-500">Forgot password?</a>
                    </div>
                    <input type="password" name="password" placeholder="••••••••"
                        class="w-full px-3.5 py-2.5 rounded-lg border border-gray-300 text-sm">
                </div>

                <!-- CAPTCHA status panel -->
                <div data-captcha-status style="font-size:12px;font-family:inherit"
                    class="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-50 border border-gray-200 text-gray-500 min-h-[36px]"></div>

                <button type="submit"
                    class="w-full py-2.5 rounded-lg bg-gray-900 text-white text-sm font-semibold">
                    Sign in
                </button>
            </form>

            <p class="mt-6 text-center text-xs text-gray-400">
                Don't have an account? <a href="#" class="text-gray-600 font-medium">Sign up</a>
            </p>
        </div>

        <p class="mt-5 text-center text-xs text-gray-400">
            Protected by <a href="https://captchaapi.eu">captchaapi.eu</a> · No cookies · EU hosted
        </p>
    </div>

    <script>
        window.CAPTCHA_BASE_URL = "https://captchaapi.eu/api/v1";
        window.CAPTCHA_SITE_KEY = "your-site-key";
    </script>
    <script src="https://captchaapi.eu/captcha.js" defer></script>

    <!-- Swap Tailwind classes when the widget updates data-captcha-state -->
    <script>
        (function () {
            const states = {
                ready:        ['bg-emerald-50', 'border-emerald-200', 'text-emerald-700'],
                error:        ['bg-red-50',     'border-red-200',     'text-red-600'],
                rate_limited: ['bg-amber-50',   'border-amber-200',   'text-amber-700'],
            };
            const base = ['bg-gray-50', 'border-gray-200', 'text-gray-500'];
            const all  = [...base, ...Object.values(states).flat()];

            const observer = new MutationObserver(function (mutations) {
                mutations.forEach(function (m) {
                    if (m.attributeName !== 'data-captcha-state') return;
                    const el = m.target;
                    const state = el.getAttribute('data-captcha-state');
                    el.classList.remove(...all);
                    el.classList.add(...(states[state] ?? base));
                });
            });

            document.addEventListener('DOMContentLoaded', function () {
                const el = document.querySelector('[data-captcha-status]');
                if (el) observer.observe(el, { attributes: true });
            });
        })();
    </script>
</body>
</html>

Related