JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 1
  • Score
    100M100P100Q20655F
  • License MIT

A fully customizable CAPTCHA component built for modern web apps

Package Exports

  • @opentech-lab/custom-captcha
  • @opentech-lab/custom-captcha/dist/index.esm.js
  • @opentech-lab/custom-captcha/dist/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@opentech-lab/custom-captcha) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Custom CAPTCHA – Build Your Own reCAPTCHA-Style Puzzle

A fully customizable CAPTCHA component built for modern web apps. Easily plug it into your forms to stop bots, test user knowledge, or filter out unwanted traffic with your own question sets or image-based puzzles.

Features

  • Protect your forms from spam and automation
  • Add your own custom questions (text, image, logic, drag-n-drop)
  • Puzzle types: multiple choice, image select, drag to match, sentence completion
  • Language & culture aware: design puzzles specific to your audience
  • Simple integration with React (other frameworks coming soon)
  • Lightweight & dependency-free (core)

Quick Start Guide

This guide will walk you through setting up the Custom CAPTCHA in a React application with an Express.js backend.

1. Installation

First, add the package to your project:

npm install @opentech-lab/custom-captcha

2. Frontend Setup (React)

In your React application, import the component and its stylesheet. Then, add the <CustomCaptcha /> component to your form.

// src/components/MyForm.tsx
import React, { useState } from 'react';
import { CustomCaptcha } from '@opentech-lab/custom-captcha';
import '@opentech-lab/custom-captcha/dist/index.css';

export default function MyForm() {
  const [captchaToken, setCaptchaToken] = useState<string | null>(null);

  // Define your CAPTCHA questions
  const questions = [
    {
      type: 'multiple-choice' as const,
      question: 'Which of these is a prime number?',
      choices: ['4', '7', '9', '12'],
      answer: '7',
    },
    {
      type: 'image-select' as const,
      question: 'Select all images containing a cat',
      images: [
        { src: '/path/to/cat1.jpg', isCorrect: true },
        { src: '/path/to/dog1.jpg', isCorrect: false },
        { src: '/path/to/cat2.jpg', isCorrect: true },
        { src: '/path/to/bird1.jpg', isCorrect: false },
      ],
    },
  ];

  const handleSubmit = async (event: React.FormEvent) => {
    event.preventDefault();
    // Your form submission logic here
    // The captchaToken is sent to the server for validation
    console.log('Submitting with token:', captchaToken);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" name="email" placeholder="Your email" required />

      <CustomCaptcha
        siteKey="YOUR_UNIQUE_SITE_KEY"
        questions={questions}
        onSuccess={(token) => {
          console.log('CAPTCHA solved:', token);
          setCaptchaToken(token);
        }}
        onError={(error) => {
          console.error('CAPTCHA failed:', error);
          setCaptchaToken(null);
        }}
        theme="light"
      />

      <button type="submit" disabled={!captchaToken}>
        Submit
      </button>
    </form>
  );
}

3. Define Your Questions Once

Create your CAPTCHA questions in a separate file for reusability:

// src/config/captchaQuestions.ts
import { CaptchaQuestion } from '@opentech-lab/custom-captcha';

export const defaultQuestions: CaptchaQuestion[] = [
  {
    type: 'multiple-choice',
    question: 'What is the capital of France?',
    choices: ['London', 'Berlin', 'Paris', 'Madrid'],
    answer: 'Paris',
  },
  {
    type: 'image-select',
    question: 'Select all images with cars',
    images: [
      { src: '/images/car1.jpg', isCorrect: true },
      { src: '/images/bike1.jpg', isCorrect: false },
      { src: '/images/car2.jpg', isCorrect: true },
      { src: '/images/tree1.jpg', isCorrect: false },
    ],
  },
];

For better security, add the CAPTCHA to your layout to prevent users from bypassing it by directly accessing other pages:

Next.js App Router Layout

// app/layout.tsx
'use client';
import React, { useState, useEffect } from 'react';
import { CaptchaDialog, isCaptchaVerified } from '@opentech-lab/custom-captcha';
import '@opentech-lab/custom-captcha/dist/index.css';
import { defaultQuestions } from '../config/captchaQuestions';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const [isVerified, setIsVerified] = useState(false);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // Check verification status on mount
    setIsVerified(isCaptchaVerified());
    setIsLoading(false);
  }, []);

  if (isLoading) {
    return (
      <html lang="en">
        <body>
          <div style={{ 
            display: 'flex', 
            justifyContent: 'center', 
            alignItems: 'center', 
            height: '100vh' 
          }}>
            Loading...
          </div>
        </body>
      </html>
    );
  }

  return (
    <html lang="en">
      <body>
        {isVerified ? (
          children
        ) : (
          <CaptchaDialog
            questions={defaultQuestions}
            siteKey="your-site-key"
            onVerified={() => setIsVerified(true)}
            onError={(error) => console.error('CAPTCHA failed:', error)}
          />
        )}
      </body>
    </html>
  );
}

Next.js Pages Router (_app.tsx)

// pages/_app.tsx
import type { AppProps } from 'next/app';
import React, { useState, useEffect } from 'react';
import { CaptchaDialog, isCaptchaVerified } from '@opentech-lab/custom-captcha';
import '@opentech-lab/custom-captcha/dist/index.css';
import { defaultQuestions } from '../config/captchaQuestions';

export default function App({ Component, pageProps }: AppProps) {
  const [isVerified, setIsVerified] = useState(false);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    setIsVerified(isCaptchaVerified());
    setIsLoading(false);
  }, []);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (!isVerified) {
    return (
      <CaptchaDialog
        questions={defaultQuestions}
        siteKey="your-site-key"
        onVerified={() => setIsVerified(true)}
        onError={(error) => console.error('CAPTCHA failed:', error)}
      />
    );
  }

  return <Component {...pageProps} />;
}

5. Simple Page-Level Implementation

If you prefer page-level control:

// src/pages/protected-page.tsx
import React, { useState } from 'react';
import { CaptchaDialog, isCaptchaVerified } from '@opentech-lab/custom-captcha';
import { defaultQuestions } from '../config/captchaQuestions';

export default function ProtectedPage() {
  const [isVerified, setIsVerified] = useState(() => isCaptchaVerified());

  if (!isVerified) {
    return (
      <CaptchaDialog
        questions={defaultQuestions}
        siteKey="your-site-key"
        onVerified={() => setIsVerified(true)}
      />
    );
  }

  return (
    <div>
      <h1>Protected Content</h1>
      <p>This content is only shown after CAPTCHA verification.</p>
    </div>
  );
}

Security Benefits

Why use layout implementation?

Prevents bypass - Users can't access other pages without verification
Site-wide protection - Protects all routes automatically
Persistent cache - 24-hour localStorage cache reduces friction
SSR compatible - Works with Next.js server-side rendering

6. Custom Implementation

If you prefer to build your own dialog, use the base CustomCaptcha component:

// src/components/MyCustomDialog.tsx
import React from 'react';
import { CustomCaptcha } from '@opentech-lab/custom-captcha';
import { defaultQuestions } from '../config/captchaQuestions';

export function MyCustomDialog({ onClose, onSuccess }) {
  return (
    <div style={{ 
      position: 'fixed', 
      top: 0, 
      left: 0, 
      right: 0, 
      bottom: 0, 
      background: 'rgba(0,0,0,0.5)',
      zIndex: 1000 
    }}>
      <div style={{ 
        margin: '50px auto', 
        background: 'white', 
        padding: '20px', 
        maxWidth: '500px',
        borderRadius: '8px'
      }}>
        <CustomCaptcha
          questions={defaultQuestions}
          siteKey="your-site-key"
          onSuccess={(token) => {
            // Cache the verification
            localStorage.setItem('captcha_verified', JSON.stringify({
              token,
              timestamp: Date.now()
            }));
            onSuccess(token);
            onClose();
          }}
          onError={(error) => console.error(error)}
        />
      </div>
    </div>
  );
}

API Reference

CustomCaptcha Props

Prop Type Required Default Description
questions CaptchaQuestion[] Yes - Array of questions (see formats below)
siteKey string Yes - Unique key for your frontend
onSuccess (token: string) => void Yes - Callback when CAPTCHA is solved
onError (error: Error) => void No - Callback on failure
theme 'light' | 'dark' No light Visual theme
className string No '' Additional CSS classes

Question Types

Multiple Choice Question

{
  type: 'multiple-choice';
  question: string;
  choices: string[];
  answer: string; // Must match one of the choices
}

Image Select Question

{
  type: 'image-select';
  question: string;
  images: Array<{
    src: string;
    alt?: string;
    isCorrect: boolean;
  }>;
  maxSelections?: number; // Optional: limit how many images can be selected
}

Troubleshooting

SSR Issues with Next.js

If you encounter "Cannot read properties of undefined (reading 'ReactCurrentDispatcher')" errors:

  1. Make sure you're using version 0.0.4 or later.
  2. Import styles: import '@opentech-lab/custom-captcha/dist/index.css'.
  3. The component automatically handles SSR by bundling the "use client"; directive. No additional configuration is needed.

TypeScript Issues

For TypeScript projects, use as const for question types to ensure proper type inference:

const questions = [
  {
    type: 'multiple-choice' as const,
    // ... rest of question
  }
];

Advanced Ideas (Coming Soon)

  • Time-based interaction analysis (anti-GPT bot)
  • reCAPTCHA-style invisible scoring
  • Crowd-verified question pools
  • Custom image puzzle builder
  • Audio CAPTCHA (for accessibility)

Ethics & Safety

This tool is designed for developers building secure and intentional communities. Use ideological or political filters responsibly. Avoid using CAPTCHAs in ways that could be discriminatory or violate local laws.

License

MIT

Contributing

PRs welcome. If you'd like to contribute new puzzle types, translations, or integrations (Vue, Svelte, etc.), feel free to open an issue or fork the repo.