Claw
import { useState } from 'react';
const css = `
@keyframes cb-claw-dip { 0%, 100% { transform: translateY(0); } 40%, 60% { transform: translateY(17px); } }
@keyframes cb-claw-drop-l { 0%, 45% { transform: rotate(-12deg); } 55%, 85% { transform: rotate(32deg); } 100% { transform: rotate(-12deg); } }
@keyframes cb-claw-drop-r { 0%, 45% { transform: rotate(12deg); } 55%, 85% { transform: rotate(-32deg); } 100% { transform: rotate(12deg); } }
@keyframes cb-claw-grab-l { 0% { transform: rotate(-12deg); } 12%, 45% { transform: rotate(32deg); } 55%, 100% { transform: rotate(-12deg); } }
@keyframes cb-claw-grab-r { 0% { transform: rotate(12deg); } 12%, 45% { transform: rotate(-32deg); } 55%, 100% { transform: rotate(12deg); } }
@keyframes cb-claw-off { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
@keyframes cb-claw-on { 0%, 49% { opacity: 0; } 50%, 100% { opacity: 1; } }
.cb-claw { position: relative; display: inline-flex; align-items: center; gap: 12px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; cursor: pointer; user-select: none; }
.cb-claw__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.cb-claw__box { position: relative; width: 44px; height: 66px; clip-path: inset(0 -12px -12px -12px); }
.cb-claw__input:focus-visible + .cb-claw__box .cb-claw__bin { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 3px; }
.cb-claw__rail { position: absolute; left: 0; right: 0; top: 0; height: 3px; border-radius: 2px; background: light-dark(#dbdde1, #2e3035); }
.cb-claw__bin {
position: absolute;
left: 7px;
bottom: 0;
width: 30px;
height: 24px;
border: 1.5px solid light-dark(#6d7179, #7d8087);
border-top: 0;
border-radius: 0 0 7px 7px;
background: light-dark(#f3f4f7, #1a1c1f);
}
.cb-claw__rig { position: absolute; left: 22px; top: 0; width: 0; height: 0; }
.cb-claw__cable { position: absolute; left: -0.75px; top: -50px; width: 1.5px; height: 60px; background: light-dark(#6d7179, #7d8087); }
.cb-claw__head { position: absolute; left: -6px; top: 9px; width: 12px; height: 6px; border-radius: 2px 2px 3px 3px; background: light-dark(#5f626a, #a7abb2); }
.cb-claw__prong { position: absolute; top: 13px; width: 2px; height: 11px; border-radius: 1px; background: light-dark(#5f626a, #a7abb2); transform-origin: 50% 0; }
.cb-claw__prong::after { content: ''; position: absolute; bottom: 0; width: 4px; height: 2px; border-radius: 1px; background: light-dark(#5f626a, #a7abb2); }
.cb-claw__prong--l { left: -5px; transform: rotate(-12deg); }
.cb-claw__prong--l::after { left: 0; }
.cb-claw__prong--r { left: 3px; transform: rotate(12deg); }
.cb-claw__prong--r::after { right: 0; }
.cb-claw__tick { position: absolute; width: 16px; height: 13px; }
.cb-claw__tick path { fill: none; stroke: light-dark(#373a40, #f1f2f3); stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; }
.cb-claw__tick--held { left: -8px; top: 19px; }
.cb-claw__tick--dropped { left: 6px; top: 7px; opacity: 0; }
.cb-claw__input:checked + .cb-claw__box .cb-claw__tick--held { opacity: 0; }
.cb-claw__input:checked + .cb-claw__box .cb-claw__tick--dropped { opacity: 1; }
.cb-claw[data-armed] .cb-claw__input:checked + .cb-claw__box .cb-claw__rig { animation: cb-claw-dip 1s ease-in-out; }
.cb-claw[data-armed] .cb-claw__input:checked + .cb-claw__box .cb-claw__prong--l { animation: cb-claw-drop-l 1s ease-in-out; }
.cb-claw[data-armed] .cb-claw__input:checked + .cb-claw__box .cb-claw__prong--r { animation: cb-claw-drop-r 1s ease-in-out; }
.cb-claw[data-armed] .cb-claw__input:checked + .cb-claw__box .cb-claw__tick--held { animation: cb-claw-off 1s linear; }
.cb-claw[data-armed] .cb-claw__input:checked + .cb-claw__box .cb-claw__tick--dropped { animation: cb-claw-on 1s linear; }
.cb-claw[data-armed] .cb-claw__input:not(:checked) + .cb-claw__box .cb-claw__rig { animation: cb-claw-dip 1s ease-in-out; }
.cb-claw[data-armed] .cb-claw__input:not(:checked) + .cb-claw__box .cb-claw__prong--l { animation: cb-claw-grab-l 1s ease-in-out; }
.cb-claw[data-armed] .cb-claw__input:not(:checked) + .cb-claw__box .cb-claw__prong--r { animation: cb-claw-grab-r 1s ease-in-out; }
.cb-claw[data-armed] .cb-claw__input:not(:checked) + .cb-claw__box .cb-claw__tick--held { animation: cb-claw-on 1s linear; }
.cb-claw[data-armed] .cb-claw__input:not(:checked) + .cb-claw__box .cb-claw__tick--dropped { animation: cb-claw-off 1s linear; }
@media (prefers-reduced-motion: reduce) {
.cb-claw__box * { animation: none !important; }
}
`;
function Tick({ className }: { className: string }) {
return (
<svg className={`cb-claw__tick ${className}`} viewBox="0 0 16 13">
<path d="M2.5 7L6.2 10.5L13.5 2.5" />
</svg>
);
}
export function ClawCheckbox() {
const [armed, setArmed] = useState(false);
return (
<>
<style>
{css}
</style>
<label className="cb-claw" data-armed={armed || undefined}>
<input className="cb-claw__input" type="checkbox" onChange={() => setArmed(true)} />
<span className="cb-claw__box" aria-hidden="true">
<span className="cb-claw__rail" />
<span className="cb-claw__bin">
<Tick className="cb-claw__tick--dropped" />
</span>
<span className="cb-claw__rig">
<span className="cb-claw__cable" />
<Tick className="cb-claw__tick--held" />
<span className="cb-claw__prong cb-claw__prong--l" />
<span className="cb-claw__prong cb-claw__prong--r" />
<span className="cb-claw__head" />
</span>
</span>
Grab this one
</label>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCbClaw(el) on the mounted element instead.
<label class="cb-claw"><input class="cb-claw__input" type="checkbox"/><span class="cb-claw__box" aria-hidden="true"><span class="cb-claw__rail"></span><span class="cb-claw__bin"><svg class="cb-claw__tick cb-claw__tick--dropped" viewBox="0 0 16 13"><path d="M2.5 7L6.2 10.5L13.5 2.5"></path></svg></span><span class="cb-claw__rig"><span class="cb-claw__cable"></span><svg class="cb-claw__tick cb-claw__tick--held" viewBox="0 0 16 13"><path d="M2.5 7L6.2 10.5L13.5 2.5"></path></svg><span class="cb-claw__prong cb-claw__prong--l"></span><span class="cb-claw__prong cb-claw__prong--r"></span><span class="cb-claw__head"></span></span></span>Grab this one</label>@keyframes cb-claw-dip { 0%, 100% { transform: translateY(0); } 40%, 60% { transform: translateY(17px); } }
@keyframes cb-claw-drop-l { 0%, 45% { transform: rotate(-12deg); } 55%, 85% { transform: rotate(32deg); } 100% { transform: rotate(-12deg); } }
@keyframes cb-claw-drop-r { 0%, 45% { transform: rotate(12deg); } 55%, 85% { transform: rotate(-32deg); } 100% { transform: rotate(12deg); } }
@keyframes cb-claw-grab-l { 0% { transform: rotate(-12deg); } 12%, 45% { transform: rotate(32deg); } 55%, 100% { transform: rotate(-12deg); } }
@keyframes cb-claw-grab-r { 0% { transform: rotate(12deg); } 12%, 45% { transform: rotate(-32deg); } 55%, 100% { transform: rotate(12deg); } }
@keyframes cb-claw-off { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
@keyframes cb-claw-on { 0%, 49% { opacity: 0; } 50%, 100% { opacity: 1; } }
.cb-claw { position: relative; display: inline-flex; align-items: center; gap: 12px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; cursor: pointer; user-select: none; }
.cb-claw__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.cb-claw__box { position: relative; width: 44px; height: 66px; clip-path: inset(0 -12px -12px -12px); }
.cb-claw__input:focus-visible + .cb-claw__box .cb-claw__bin { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 3px; }
.cb-claw__rail { position: absolute; left: 0; right: 0; top: 0; height: 3px; border-radius: 2px; background: light-dark(#dbdde1, #2e3035); }
.cb-claw__bin {
position: absolute;
left: 7px;
bottom: 0;
width: 30px;
height: 24px;
border: 1.5px solid light-dark(#6d7179, #7d8087);
border-top: 0;
border-radius: 0 0 7px 7px;
background: light-dark(#f3f4f7, #1a1c1f);
}
.cb-claw__rig { position: absolute; left: 22px; top: 0; width: 0; height: 0; }
.cb-claw__cable { position: absolute; left: -0.75px; top: -50px; width: 1.5px; height: 60px; background: light-dark(#6d7179, #7d8087); }
.cb-claw__head { position: absolute; left: -6px; top: 9px; width: 12px; height: 6px; border-radius: 2px 2px 3px 3px; background: light-dark(#5f626a, #a7abb2); }
.cb-claw__prong { position: absolute; top: 13px; width: 2px; height: 11px; border-radius: 1px; background: light-dark(#5f626a, #a7abb2); transform-origin: 50% 0; }
.cb-claw__prong::after { content: ''; position: absolute; bottom: 0; width: 4px; height: 2px; border-radius: 1px; background: light-dark(#5f626a, #a7abb2); }
.cb-claw__prong--l { left: -5px; transform: rotate(-12deg); }
.cb-claw__prong--l::after { left: 0; }
.cb-claw__prong--r { left: 3px; transform: rotate(12deg); }
.cb-claw__prong--r::after { right: 0; }
.cb-claw__tick { position: absolute; width: 16px; height: 13px; }
.cb-claw__tick path { fill: none; stroke: light-dark(#373a40, #f1f2f3); stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; }
.cb-claw__tick--held { left: -8px; top: 19px; }
.cb-claw__tick--dropped { left: 6px; top: 7px; opacity: 0; }
.cb-claw__input:checked + .cb-claw__box .cb-claw__tick--held { opacity: 0; }
.cb-claw__input:checked + .cb-claw__box .cb-claw__tick--dropped { opacity: 1; }
.cb-claw[data-armed] .cb-claw__input:checked + .cb-claw__box .cb-claw__rig { animation: cb-claw-dip 1s ease-in-out; }
.cb-claw[data-armed] .cb-claw__input:checked + .cb-claw__box .cb-claw__prong--l { animation: cb-claw-drop-l 1s ease-in-out; }
.cb-claw[data-armed] .cb-claw__input:checked + .cb-claw__box .cb-claw__prong--r { animation: cb-claw-drop-r 1s ease-in-out; }
.cb-claw[data-armed] .cb-claw__input:checked + .cb-claw__box .cb-claw__tick--held { animation: cb-claw-off 1s linear; }
.cb-claw[data-armed] .cb-claw__input:checked + .cb-claw__box .cb-claw__tick--dropped { animation: cb-claw-on 1s linear; }
.cb-claw[data-armed] .cb-claw__input:not(:checked) + .cb-claw__box .cb-claw__rig { animation: cb-claw-dip 1s ease-in-out; }
.cb-claw[data-armed] .cb-claw__input:not(:checked) + .cb-claw__box .cb-claw__prong--l { animation: cb-claw-grab-l 1s ease-in-out; }
.cb-claw[data-armed] .cb-claw__input:not(:checked) + .cb-claw__box .cb-claw__prong--r { animation: cb-claw-grab-r 1s ease-in-out; }
.cb-claw[data-armed] .cb-claw__input:not(:checked) + .cb-claw__box .cb-claw__tick--held { animation: cb-claw-on 1s linear; }
.cb-claw[data-armed] .cb-claw__input:not(:checked) + .cb-claw__box .cb-claw__tick--dropped { animation: cb-claw-off 1s linear; }
@media (prefers-reduced-motion: reduce) {
.cb-claw__box * { animation: none !important; }
}function initCbClaw(root) {
var input = root.querySelector('input');
if (!input) return;
// Animate only after the first change, so nothing plays on page load.
function arm() { root.setAttribute('data-armed', ''); }
input.addEventListener('change', arm);
return function () { input.removeEventListener('change', arm); };
}
document.querySelectorAll('.cb-claw').forEach(initCbClaw);Goal: Create a "Claw" checkbox.
Visual behavior: A claw machine checkbox: ticking lowers the claw into the bin, opens it and lets the tick drop; unticking sends it back down to grab the tick and lift it out again.
Technique:
- A real native checkbox (visually hidden: absolute, 1x1, opacity 0) inside a <label>; every visual is aria-hidden and styled from the input with :checked + sibling selectors.
- One rig (cable, head, two prongs and a held tick) dips on a keyframe; the prongs open and close on their own keyframes, different for dropping and grabbing.
- There are two ticks, one held by the claw and one in the bin; they swap visibility at the bottom of the dip, so the tick looks handed over. A clip-path on the box hides the cable above it.
- The keyframes only apply once a data-armed flag is set on the first change, so nothing plays on page load; ticking and unticking use differently named keyframes, so each change restarts the right one.
Accessibility: A native checkbox inside a <label>, so Space toggles it and the visible label names it; :focus-visible on the input outlines the visual. Under prefers-reduced-motion the tick simply moves between the claw and the bin.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Claw" checkbox.
Visual behavior: A claw machine checkbox: ticking lowers the claw into the bin, opens it and lets the tick drop; unticking sends it back down to grab the tick and lift it out again.
Technique:
- A real native checkbox (visually hidden: absolute, 1x1, opacity 0) inside a <label>; every visual is aria-hidden and styled from the input with :checked + sibling selectors.
- One rig (cable, head, two prongs and a held tick) dips on a keyframe; the prongs open and close on their own keyframes, different for dropping and grabbing.
- There are two ticks, one held by the claw and one in the bin; they swap visibility at the bottom of the dip, so the tick looks handed over. A clip-path on the box hides the cable above it.
- The keyframes only apply once a data-armed flag is set on the first change, so nothing plays on page load; ticking and unticking use differently named keyframes, so each change restarts the right one.
Accessibility: A native checkbox inside a <label>, so Space toggles it and the visible label names it; :focus-visible on the input outlines the visual. Under prefers-reduced-motion the tick simply moves between the claw and the bin.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A claw machine checkbox: ticking lowers the claw into the bin, opens it and lets the tick drop; unticking sends it back down to grab the tick and lift it out again.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change
- Techniques
- keyframes, clip path, state machine
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native checkbox inside a <label>, so Space toggles it and the visible label names it; :focus-visible on the input outlines the visual. Under prefers-reduced-motion the tick simply moves between the claw and the bin.