Checkmate
import { useState } from 'react';
const HW = 28;
const HH = 14;
const TOP = 62;
const CX = 128;
const at = (c: number, r: number) => ({ x: CX + (c - r) * HW, y: TOP + (c + r + 1) * HH });
const CELLS = Array.from({ length: 16 }, (_, i) => {
const c = i % 4;
const r = Math.floor(i / 4);
const { x, y } = at(c, r);
return { key: i, dark: (c + r) % 2 === 1, d: `M${x} ${y - HH}L${x + HW} ${y}L${x} ${y + HH}L${x - HW} ${y}Z` };
});
const KING = at(2, 1);
const FROM = at(0, 3);
const TO = at(1, 2);
const PAWN = at(3, 3);
const ROOK = at(0, 0);
const css = `
.cb-checkmate { --light-sq: light-dark(#ebedf0, #a7abb2); --dark-sq: light-dark(#5f626a, #2e3035); --edge: light-dark(#50535b, #232529); --edge2: light-dark(#42454c, #1a1c1f); --ivory: light-dark(#fbfcfd, #f1f2f3); --ebony: light-dark(#373a40, #121417); --from: translate(${FROM.x}px, ${FROM.y}px); --to: translate(${TO.x}px, ${TO.y}px); flex: none; position: relative; display: flex; flex-direction: column; align-items: center; gap: 4px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; font-weight: 500; cursor: pointer; -webkit-user-select: none; user-select: none; }
.cb-checkmate__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.cb-checkmate__art { display: block; width: 256px; height: 192px; overflow: visible; border-radius: 12px; }
.cb-checkmate__input:focus-visible + .cb-checkmate__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 2px; }
.cb-checkmate__sq--l { fill: var(--light-sq); }
.cb-checkmate__sq--d { fill: var(--dark-sq); }
.cb-checkmate__side-a { fill: var(--edge); }
.cb-checkmate__side-b { fill: var(--edge2); }
.cb-checkmate__shade { fill: var(--ebony); opacity: 0.28; }
.cb-checkmate__ivory { fill: var(--ivory); stroke: var(--ebony); stroke-width: 1.3; stroke-linejoin: round; }
.cb-checkmate__ebony { fill: var(--ebony); stroke: var(--ivory); stroke-width: 1.3; stroke-linejoin: round; }
.cb-checkmate__queen { transform: var(--from); transition: transform 0.42s cubic-bezier(0.4, 0, 0.3, 1); }
.cb-checkmate__input:checked + .cb-checkmate__art .cb-checkmate__queen { transform: var(--to); transition: transform 0.5s cubic-bezier(0.3, 0.9, 0.3, 1); }
.cb-checkmate__lift { transition: transform 0.2s; }
.cb-checkmate:hover .cb-checkmate__lift { transform: translateY(-5px); }
.cb-checkmate__input:checked + .cb-checkmate__art .cb-checkmate__lift { transform: none; }
.cb-checkmate__tip { transform-origin: 14px 0; transition: transform 0.5s cubic-bezier(0.3, 1.5, 0.5, 1); }
.cb-checkmate__input:checked + .cb-checkmate__art .cb-checkmate__tip { transform: rotate(82deg); }
.cb-checkmate[data-armed] .cb-checkmate__input:checked + .cb-checkmate__art .cb-checkmate__tip { animation: cb-checkmate-fall 1.15s linear; }
@keyframes cb-checkmate-fall {
0%, 38% { transform: none; }
46% { transform: rotate(-7deg); }
54% { transform: rotate(6deg); }
62% { transform: rotate(-4deg); }
68% { transform: rotate(3deg); animation-timing-function: cubic-bezier(0.5, 0, 1, 0.6); }
84% { transform: rotate(82deg); }
90% { transform: rotate(72deg); animation-timing-function: cubic-bezier(0.5, 0, 1, 0.6); }
96% { transform: rotate(82deg); }
98% { transform: rotate(79deg); }
100% { transform: rotate(82deg); }
}
.cb-checkmate__call { position: absolute; left: 0; right: 0; top: 0; color: light-dark(#373a40, #f1f2f3); font: italic 700 24px/1 Georgia, 'Times New Roman', serif; letter-spacing: -0.01em; text-align: center; opacity: 0; transform: scale(0.7); transition: opacity 0.15s, transform 0.15s; pointer-events: none; }
.cb-checkmate__input:checked ~ .cb-checkmate__call { opacity: 1; transform: none; transition: opacity 0.2s 0.95s, transform 0.34s cubic-bezier(0.3, 1.7, 0.5, 1) 0.95s; }
@media (prefers-reduced-motion: reduce) {
.cb-checkmate *, .cb-checkmate[data-armed] .cb-checkmate__input + .cb-checkmate__art * { animation: none !important; transition: none !important; }
}
`;
function Base() {
return <path d="M-15 0C-15 -6 15 -6 15 0C15 4 -15 4 -15 0Z" />;
}
export function CheckmateCheckbox() {
const [armed, setArmed] = useState(false);
return (
<>
<style>
{css}
</style>
<label className="cb-checkmate" data-armed={armed || undefined}>
<input className="cb-checkmate__input" type="checkbox" onChange={() => setArmed(true)} />
<svg className="cb-checkmate__art" viewBox="0 0 256 192" aria-hidden="true">
<path className="cb-checkmate__side-a" d={`M${CX - HW * 4} ${TOP + HH * 4}L${CX} ${TOP + HH * 8}V${TOP + HH * 8 + 10}L${CX - HW * 4} ${TOP + HH * 4 + 10}Z`} />
<path className="cb-checkmate__side-b" d={`M${CX + HW * 4} ${TOP + HH * 4}L${CX} ${TOP + HH * 8}V${TOP + HH * 8 + 10}L${CX + HW * 4} ${TOP + HH * 4 + 10}Z`} />
{CELLS.map((c) => (
<path key={c.key} className={c.dark ? 'cb-checkmate__sq--d' : 'cb-checkmate__sq--l'} d={c.d} />
))}
{/* A rook and a pawn only set the scene. */}
<g transform={`translate(${ROOK.x} ${ROOK.y})`} className="cb-checkmate__ebony">
<Base />
<path d="M-9 -3C-6 -14 -6 -24 -8 -30H8C6 -24 6 -14 9 -3Z" />
<path d="M-11 -30V-41H-6V-37H-2V-41H2V-37H6V-41H11V-30Z" />
</g>
<g className="cb-checkmate__queen">
<ellipse className="cb-checkmate__shade" rx="17" ry="6" />
<g className="cb-checkmate__lift cb-checkmate__ebony">
<Base />
<path d="M-11 -3C-6 -16 -5 -30 -8 -38H8C5 -30 6 -16 11 -3Z" />
<path d="M-10 -38H10L14 -54L6 -44L3 -58L0 -44L-3 -58L-6 -44L-14 -54Z" />
<circle cx="0" cy="-60" r="3" />
</g>
</g>
<g transform={`translate(${KING.x} ${KING.y})`}>
<ellipse className="cb-checkmate__shade" rx="17" ry="6" />
<g className="cb-checkmate__tip cb-checkmate__ivory">
<Base />
<path d="M-11 -3C-6 -16 -5 -32 -8 -40H8C5 -32 6 -16 11 -3Z" />
<rect x="-11" y="-44" width="22" height="4.5" rx="2" />
<path d="M-10 -44C-15 -56 -6 -62 0 -57C6 -62 15 -56 10 -44Z" />
<path d="M-1.8 -70H1.8V-66.5H5.4V-63H1.8V-57.5H-1.8V-63H-5.4V-66.5H-1.8Z" />
</g>
</g>
<g transform={`translate(${PAWN.x} ${PAWN.y})`} className="cb-checkmate__ivory">
<path d="M-11 0C-11 -4 11 -4 11 0C11 3 -11 3 -11 0Z" />
<path d="M-7 -2C-4 -9 -4 -14 -5 -18H5C4 -14 4 -9 7 -2Z" />
<circle cx="0" cy="-24" r="7" />
</g>
</svg>
<span className="cb-checkmate__call" aria-hidden="true">
Checkmate
</span>
Mark as won
</label>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCbCheckmate(el) on the mounted element instead.
<label class="cb-checkmate">
<input class="cb-checkmate__input" type="checkbox"/>
<svg class="cb-checkmate__art" viewBox="0 0 256 192" aria-hidden="true">
<path class="cb-checkmate__side-a" d="M16 118L128 174V184L16 128Z"></path>
<path class="cb-checkmate__side-b" d="M240 118L128 174V184L240 128Z"></path>
<path class="cb-checkmate__sq--l" d="M128 62L156 76L128 90L100 76Z"></path>
<path class="cb-checkmate__sq--d" d="M156 76L184 90L156 104L128 90Z"></path>
<path class="cb-checkmate__sq--l" d="M184 90L212 104L184 118L156 104Z"></path>
<path class="cb-checkmate__sq--d" d="M212 104L240 118L212 132L184 118Z"></path>
<path class="cb-checkmate__sq--d" d="M100 76L128 90L100 104L72 90Z"></path>
<path class="cb-checkmate__sq--l" d="M128 90L156 104L128 118L100 104Z"></path>
<path class="cb-checkmate__sq--d" d="M156 104L184 118L156 132L128 118Z"></path>
<path class="cb-checkmate__sq--l" d="M184 118L212 132L184 146L156 132Z"></path>
<path class="cb-checkmate__sq--l" d="M72 90L100 104L72 118L44 104Z"></path>
<path class="cb-checkmate__sq--d" d="M100 104L128 118L100 132L72 118Z"></path>
<path class="cb-checkmate__sq--l" d="M128 118L156 132L128 146L100 132Z"></path>
<path class="cb-checkmate__sq--d" d="M156 132L184 146L156 160L128 146Z"></path>
<path class="cb-checkmate__sq--d" d="M44 104L72 118L44 132L16 118Z"></path>
<path class="cb-checkmate__sq--l" d="M72 118L100 132L72 146L44 132Z"></path>
<path class="cb-checkmate__sq--d" d="M100 132L128 146L100 160L72 146Z"></path>
<path class="cb-checkmate__sq--l" d="M128 146L156 160L128 174L100 160Z"></path>
<g transform="translate(128 76)" class="cb-checkmate__ebony">
<path d="M-15 0C-15 -6 15 -6 15 0C15 4 -15 4 -15 0Z"></path>
<path d="M-9 -3C-6 -14 -6 -24 -8 -30H8C6 -24 6 -14 9 -3Z"></path>
<path d="M-11 -30V-41H-6V-37H-2V-41H2V-37H6V-41H11V-30Z"></path>
</g>
<g class="cb-checkmate__queen">
<ellipse class="cb-checkmate__shade" rx="17" ry="6"></ellipse>
<g class="cb-checkmate__lift cb-checkmate__ebony">
<path d="M-15 0C-15 -6 15 -6 15 0C15 4 -15 4 -15 0Z"></path>
<path d="M-11 -3C-6 -16 -5 -30 -8 -38H8C5 -30 6 -16 11 -3Z"></path>
<path d="M-10 -38H10L14 -54L6 -44L3 -58L0 -44L-3 -58L-6 -44L-14 -54Z"></path>
<circle cx="0" cy="-60" r="3"></circle>
</g>
</g>
<g transform="translate(156 118)">
<ellipse class="cb-checkmate__shade" rx="17" ry="6"></ellipse>
<g class="cb-checkmate__tip cb-checkmate__ivory">
<path d="M-15 0C-15 -6 15 -6 15 0C15 4 -15 4 -15 0Z"></path>
<path d="M-11 -3C-6 -16 -5 -32 -8 -40H8C5 -32 6 -16 11 -3Z"></path>
<rect x="-11" y="-44" width="22" height="4.5" rx="2"></rect>
<path d="M-10 -44C-15 -56 -6 -62 0 -57C6 -62 15 -56 10 -44Z"></path>
<path d="M-1.8 -70H1.8V-66.5H5.4V-63H1.8V-57.5H-1.8V-63H-5.4V-66.5H-1.8Z"></path>
</g>
</g>
<g transform="translate(128 160)" class="cb-checkmate__ivory">
<path d="M-11 0C-11 -4 11 -4 11 0C11 3 -11 3 -11 0Z"></path>
<path d="M-7 -2C-4 -9 -4 -14 -5 -18H5C4 -14 4 -9 7 -2Z"></path>
<circle cx="0" cy="-24" r="7"></circle>
</g>
</svg>
<span class="cb-checkmate__call" aria-hidden="true">Checkmate</span>
Mark as won
</label>.cb-checkmate { --light-sq: light-dark(#ebedf0, #a7abb2); --dark-sq: light-dark(#5f626a, #2e3035); --edge: light-dark(#50535b, #232529); --edge2: light-dark(#42454c, #1a1c1f); --ivory: light-dark(#fbfcfd, #f1f2f3); --ebony: light-dark(#373a40, #121417); --from: translate(44px, 118px); --to: translate(100px, 118px); flex: none; position: relative; display: flex; flex-direction: column; align-items: center; gap: 4px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; font-weight: 500; cursor: pointer; -webkit-user-select: none; user-select: none; }
.cb-checkmate__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.cb-checkmate__art { display: block; width: 256px; height: 192px; overflow: visible; border-radius: 12px; }
.cb-checkmate__input:focus-visible + .cb-checkmate__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 2px; }
.cb-checkmate__sq--l { fill: var(--light-sq); }
.cb-checkmate__sq--d { fill: var(--dark-sq); }
.cb-checkmate__side-a { fill: var(--edge); }
.cb-checkmate__side-b { fill: var(--edge2); }
.cb-checkmate__shade { fill: var(--ebony); opacity: 0.28; }
.cb-checkmate__ivory { fill: var(--ivory); stroke: var(--ebony); stroke-width: 1.3; stroke-linejoin: round; }
.cb-checkmate__ebony { fill: var(--ebony); stroke: var(--ivory); stroke-width: 1.3; stroke-linejoin: round; }
.cb-checkmate__queen { transform: var(--from); transition: transform 0.42s cubic-bezier(0.4, 0, 0.3, 1); }
.cb-checkmate__input:checked + .cb-checkmate__art .cb-checkmate__queen { transform: var(--to); transition: transform 0.5s cubic-bezier(0.3, 0.9, 0.3, 1); }
.cb-checkmate__lift { transition: transform 0.2s; }
.cb-checkmate:hover .cb-checkmate__lift { transform: translateY(-5px); }
.cb-checkmate__input:checked + .cb-checkmate__art .cb-checkmate__lift { transform: none; }
.cb-checkmate__tip { transform-origin: 14px 0; transition: transform 0.5s cubic-bezier(0.3, 1.5, 0.5, 1); }
.cb-checkmate__input:checked + .cb-checkmate__art .cb-checkmate__tip { transform: rotate(82deg); }
.cb-checkmate[data-armed] .cb-checkmate__input:checked + .cb-checkmate__art .cb-checkmate__tip { animation: cb-checkmate-fall 1.15s linear; }
@keyframes cb-checkmate-fall {
0%, 38% { transform: none; }
46% { transform: rotate(-7deg); }
54% { transform: rotate(6deg); }
62% { transform: rotate(-4deg); }
68% { transform: rotate(3deg); animation-timing-function: cubic-bezier(0.5, 0, 1, 0.6); }
84% { transform: rotate(82deg); }
90% { transform: rotate(72deg); animation-timing-function: cubic-bezier(0.5, 0, 1, 0.6); }
96% { transform: rotate(82deg); }
98% { transform: rotate(79deg); }
100% { transform: rotate(82deg); }
}
.cb-checkmate__call { position: absolute; left: 0; right: 0; top: 0; color: light-dark(#373a40, #f1f2f3); font: italic 700 24px/1 Georgia, 'Times New Roman', serif; letter-spacing: -0.01em; text-align: center; opacity: 0; transform: scale(0.7); transition: opacity 0.15s, transform 0.15s; pointer-events: none; }
.cb-checkmate__input:checked ~ .cb-checkmate__call { opacity: 1; transform: none; transition: opacity 0.2s 0.95s, transform 0.34s cubic-bezier(0.3, 1.7, 0.5, 1) 0.95s; }
@media (prefers-reduced-motion: reduce) {
.cb-checkmate *, .cb-checkmate[data-armed] .cb-checkmate__input + .cb-checkmate__art * { animation: none !important; transition: none !important; }
}function initCbCheckmate(root) {
var input = root.querySelector('.cb-checkmate__input');
// The king only falls when someone ticks it, not on load.
function onChange() {
root.setAttribute('data-armed', 'true');
}
input.addEventListener('change', onChange);
return function () {
input.removeEventListener('change', onChange);
};
}
document.querySelectorAll('.cb-checkmate').forEach(initCbCheckmate);Goal: Create a "Checkmate" checkbox.
Visual behavior: Tick it and the queen glides across the board, the king wobbles and topples over: checkmate. Untick and the king stands back up.
Technique:
- The board is a 4 by 4 isometric grid and every piece is placed from the same function, so the queen only needs a from and a to position, both held in CSS variables.
- One keyframe times the king to the queen: it waits for her to arrive, rocks from side to side, falls about one corner of its base and bounces once on its side. A data-armed flag goes on at the first change, so the fall never plays on load. Unticking transitions the king upright.
Accessibility: A native checkbox inside a <label>, so "Mark as won" is its name. The board and the word Checkmate are aria-hidden, and keyboard focus outlines the board. Under prefers-reduced-motion the pieces go to their places at once.
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 "Checkmate" checkbox.
Visual behavior: Tick it and the queen glides across the board, the king wobbles and topples over: checkmate. Untick and the king stands back up.
Technique:
- The board is a 4 by 4 isometric grid and every piece is placed from the same function, so the queen only needs a from and a to position, both held in CSS variables.
- One keyframe times the king to the queen: it waits for her to arrive, rocks from side to side, falls about one corner of its base and bounces once on its side. A data-armed flag goes on at the first change, so the fall never plays on load. Unticking transitions the king upright.
Accessibility: A native checkbox inside a <label>, so "Mark as won" is its name. The board and the word Checkmate are aria-hidden, and keyboard focus outlines the board. Under prefers-reduced-motion the pieces go to their places at once.
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
Tick it and the queen glides across the board, the king wobbles and topples over: checkmate. Untick and the king stands back up.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, keyframe
- Techniques
- keyframes, svg, css transition, css variables
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native checkbox inside a <label>, so "Mark as won" is its name. The board and the word Checkmate are aria-hidden, and keyboard focus outlines the board. Under prefers-reduced-motion the pieces go to their places at once.