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-captcha2. 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. Popup Dialog with localStorage Cache
The CAPTCHA works as a popup dialog that caches successful verification in localStorage for 24 hours, reducing user friction.
// src/components/CaptchaDialog.tsx
import React, { useState, useEffect } from 'react';
import { CustomCaptcha } from '@opentech-lab/custom-captcha';
import '@opentech-lab/custom-captcha/dist/index.css';
const CACHE_KEY = 'captcha_verified';
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
export function CaptchaDialog({ onVerified }: { onVerified: () => void }) {
const [showCaptcha, setShowCaptcha] = useState(false);
useEffect(() => {
// Check if user has already verified within the last 24 hours
const cachedVerification = localStorage.getItem(CACHE_KEY);
if (cachedVerification) {
const verificationData = JSON.parse(cachedVerification);
const now = Date.now();
if (now - verificationData.timestamp < CACHE_DURATION) {
// Still valid, user doesn't need to verify again
onVerified();
return;
} else {
// Cache expired, remove it
localStorage.removeItem(CACHE_KEY);
}
}
// Show CAPTCHA dialog
setShowCaptcha(true);
}, [onVerified]);
const handleCaptchaSuccess = (token: string) => {
// Store verification in localStorage with timestamp
const verificationData = {
token,
timestamp: Date.now(),
};
localStorage.setItem(CACHE_KEY, JSON.stringify(verificationData));
setShowCaptcha(false);
onVerified();
};
const questions = [
{
type: 'multiple-choice' as const,
question: 'What is 5 + 3?',
choices: ['6', '7', '8', '9'],
answer: '8',
},
];
if (!showCaptcha) return null;
return (
<div className="captcha-overlay">
<div className="captcha-dialog">
<h3>Please verify you're human</h3>
<CustomCaptcha
siteKey="your-site-key"
questions={questions}
onSuccess={handleCaptchaSuccess}
onError={(error) => console.error('CAPTCHA failed:', error)}
theme="light"
/>
</div>
</div>
);
}
// Add these styles to your CSS
const styles = `
.captcha-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.captcha-dialog {
background: white;
padding: 2rem;
border-radius: 8px;
max-width: 500px;
width: 90%;
}
`;4. Usage in Your App
// src/App.tsx
import React, { useState } from 'react';
import { CaptchaDialog } from './components/CaptchaDialog';
export default function App() {
const [isVerified, setIsVerified] = useState(false);
const [showProtectedContent, setShowProtectedContent] = useState(false);
const handleAccessProtectedContent = () => {
if (isVerified) {
setShowProtectedContent(true);
} else {
// This will trigger the CAPTCHA dialog if not cached
setIsVerified(false);
}
};
return (
<div>
<h1>My Website</h1>
<button onClick={handleAccessProtectedContent}>
Access Protected Content
</button>
{showProtectedContent && (
<div>
<h2>Protected Content</h2>
<p>This content is only shown after CAPTCHA verification.</p>
</div>
)}
{!isVerified && (
<CaptchaDialog onVerified={() => setIsVerified(true)} />
)}
</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:
- Make sure you're using version
0.0.4or later. - Import styles:
import '@opentech-lab/custom-captcha/dist/index.css'. - 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.