Checkmate

No dependencies
Code format
React + Tailwind
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>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/checkbox-checkmate
Source on GitHub

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.

More checkboxes

See all
Cheque
Door hanger
Hatch
Icing