An interactive 3D Web Component captcha UI inspired by the (in)famous "Square Hole" meme. Users must drag and drop 3D shapes into their incorrect holes to pass the verification. Built with Lit, Three.js, and CSG (Constructive Solid Geometry).
Warning
This component provides the UI for a puzzle. Because the verification happens in the browser, it can be bypassed via the developer console. Do not use this as your sole line of defense against bot attacks on sensitive endpoints.
- Built as a standard Web Component using Lit, it just works anywhere.
- Uses Three.js for real-time rendering and physics interactions.
- Native dark mode support, customizable colors using CSS variables and attributes.
- Portal based popup for the puzzle, no messing around with the z-index.
- Install the package and its dependencies via npm, yarn, or pnpm.
npm install @r3dacted42/shape-puzzle-captcha
- Import it once in the file containing your application's main entry point (e.g., main.js, app.js, or index.js) to register the Web Component:
import "@r3dacted42/shape-puzzle-captcha";
Support for SSR depends on whether an integratio for Lit is available. If you're using Astro or something similar, consider using the CDN method below.
If you aren't using a bundler, you can use the component directly in the browser. Because modern ES modules are used, you must include an import map to resolve the dependencies.
<script type="importmap">
{
"imports": {
"lit": "https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js",
"three": "https://unpkg.com/three@0.183.2/build/three.module.js",
"three-bvh-csg": "https://unpkg.com/three-bvh-csg@0.0.18/build/index.module.js",
"three-mesh-bvh": "https://unpkg.com/three-mesh-bvh@0.9.9/build/index.module.js"
}
}
</script>
<script
type="module"
src="https://unpkg.com/@r3dacted42/shape-puzzle-captcha@latest/dist/shape-puzzle-captcha.js"
></script>Alternatively, if you can't or don't want to use import maps, you can use the bundle version:
<script
type="module"
src="https://unpkg.com/@r3dacted42/shape-puzzle-captcha@latest/dist/bundle/shape-puzzle-captcha.js"
></script>Once registered, use the <shape-puzzle-captcha> tag anywhere in your HTML or JSX templates, and listen for events.
<shape-puzzle-captcha auto-dark> </shape-puzzle-captcha>
<button id="submit-btn" disabled>Humans Only Please!</button>
<script>
const submitBtn = document.getElementById("submit-btn");
document.addEventListener("shapepuzzlecaptcha:solved", (e) => {
// A backend is essential for a truly secure captcha!
// if (someBackendCall(clientInfo).isVerified)
submitBtn.disabled = false;
});
</script>Tip
Use class="dark" or data-dark on <shape-puzzle-captcha> to use the dark color scheme. See Advanced Theming for details. The auto-dark attribute uses class="dark" by default. Set it to "data" to use the data-dark attribute for dark theme.
| Method | Returns | Description |
|---|---|---|
reset() |
undefined |
Reset the 3D scene and returns the component to the "unsolved" state. Also emits the shapepuzzlecaptcha:reset event. |
The component emits global events that bubble up to the document/window.
Tip
The event names are dynamically prefixed by your event-key, so if you have event-key="meow" on the component, then you'll get events like meow:solved, meow:failed, meow:reset, etc.
The component uses CSS variables for easy styling. You can override these variables on the <shape-puzzle-captcha> element to match your brand. The internal popup component will automatically inherit these styles.
shape-puzzle-captcha {
--font-family: system-ui, -apple-system, sans-serif;
--bg-color: #ffffff;
--canvas-bg-color: #f0f0f0;
--text-color: #000;
--primary-color: #1a73e9;
--on-primary-color: #ffffff;
--primary-hover-color: #1669c1;
--border-color: #cccccc;
--image-btn-color: #737373;
}
shape-puzzle-captcha(.dark),
shape-puzzle-captcha([data-dark]) {
--bg-color: #1f1f1f;
--canvas-bg-color: #292929;
--text-color: #ffffff;
--primary-color: #611c99;
--on-primary-color: #ffffff;
--primary-hover-color: #6e16c1;
--border-color: #505050;
--image-btn-color: #8d8d8d;
}MIT License © 2026 r3dacted42
