Chess clock

No dependencies
Code format
React + Tailwind
import type { CSSProperties } from 'react';

const css = `
  @keyframes toggle-chess-clock-tick { to { transform: rotate(360deg); } }
  .toggle-chess-clock { display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 15px; cursor: pointer; user-select: none; }
  .toggle-chess-clock__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
  .toggle-chess-clock__art { display: block; width: 132px; height: 84px; overflow: visible; border-radius: 8px; }
  .toggle-chess-clock__input:focus-visible + .toggle-chess-clock__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
  .toggle-chess-clock__case { fill: light-dark(#dbdde1, #2e3035); }
  .toggle-chess-clock__panel { fill: light-dark(#ebedf0, #232529); }
  .toggle-chess-clock__face { fill: light-dark(#f3f4f7, #1a1c1f); stroke: light-dark(#cbced3, #393c41); stroke-width: 1.2; }
  .toggle-chess-clock__tick { stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.2; stroke-linecap: round; }
  .toggle-chess-clock__minute { stroke: light-dark(#50535b, #c6c8cd); stroke-width: 1.8; stroke-linecap: round; }
  .toggle-chess-clock__second { stroke: light-dark(#373a40, #f1f2f3); stroke-width: 1; stroke-linecap: round; transform-box: view-box; transform-origin: var(--o); animation: toggle-chess-clock-tick 8s steps(60) infinite paused; }
  .toggle-chess-clock__pin { fill: light-dark(#373a40, #f1f2f3); }
  .toggle-chess-clock__stem { fill: light-dark(#cbced3, #393c41); }
  .toggle-chess-clock__cap { fill: light-dark(#5f626a, #a7abb2); }
  /* Pressing one plunger lifts the other: its own clock stops, the other side's starts. */
  .toggle-chess-clock__plunger { transition: transform 0.18s cubic-bezier(0.3, 1.5, 0.5, 1); }
  .toggle-chess-clock__plunger--b { transform: translateY(5px); }
  .toggle-chess-clock__input:checked + .toggle-chess-clock__art .toggle-chess-clock__plunger--a { transform: translateY(5px); }
  .toggle-chess-clock__input:checked + .toggle-chess-clock__art .toggle-chess-clock__plunger--b { transform: none; }
  .toggle-chess-clock__second--a,
  .toggle-chess-clock__input:checked + .toggle-chess-clock__art .toggle-chess-clock__second--b { animation-play-state: running; }
  .toggle-chess-clock__input:checked + .toggle-chess-clock__art .toggle-chess-clock__second--a { animation-play-state: paused; }
  .toggle-chess-clock__flag { fill: light-dark(#50535b, #c6c8cd); opacity: 0; transition: opacity 0.2s; }
  .toggle-chess-clock__flag--a, .toggle-chess-clock__input:checked + .toggle-chess-clock__art .toggle-chess-clock__flag--b { opacity: 1; }
  .toggle-chess-clock__input:checked + .toggle-chess-clock__art .toggle-chess-clock__flag--a { opacity: 0; }
  @media (prefers-reduced-motion: reduce) {
    .toggle-chess-clock__second { animation: none; }
    .toggle-chess-clock__plunger { transition: none; }
  }
`;

const SIDES = [
  { k: 'a', cx: 40, minute: -50 },
  { k: 'b', cx: 80, minute: 70 },
];

export function ChessClockToggle() {
  return (
    <>
      <style>
        {css}
      </style>
      <label className="toggle-chess-clock">
        <input className="toggle-chess-clock__input" type="checkbox" role="switch" />
        <svg className="toggle-chess-clock__art" viewBox="0 0 120 76" aria-hidden="true">
          {SIDES.map((s) => (
            <g key={s.k} className={`toggle-chess-clock__plunger toggle-chess-clock__plunger--${s.k}`}>
              <rect className="toggle-chess-clock__stem" x={s.cx - 3} y="14" width="6" height="10" />
              <rect className="toggle-chess-clock__cap" x={s.cx - 10} y="10" width="20" height="7" rx="2.5" />
            </g>
          ))}
          <path className="toggle-chess-clock__case" d="M8 72L16 28Q17 24 21 24H99Q103 24 104 28L112 72Z" />
          <rect className="toggle-chess-clock__panel" x="17" y="31" width="86" height="33" rx="5" />
          {SIDES.map((s) => (
            <g key={s.k}>
              <circle className="toggle-chess-clock__face" cx={s.cx} cy="47" r="13" />
              {[0, 90, 180, 270].map((a) => (
                <line key={a} className="toggle-chess-clock__tick" x1={s.cx} y1="36" x2={s.cx} y2="38" transform={`rotate(${a} ${s.cx} 47)`} />
              ))}
              {/* The flag stands up on the clock that is running. */}
              <path className={`toggle-chess-clock__flag toggle-chess-clock__flag--${s.k}`} d={`M${s.cx + 3} 37L${s.cx + 7} 38L${s.cx + 3} 40Z`} />
              <line className="toggle-chess-clock__minute" x1={s.cx} y1="47" x2={s.cx} y2="40" transform={`rotate(${s.minute} ${s.cx} 47)`} />
              <line
                className={`toggle-chess-clock__second toggle-chess-clock__second--${s.k}`}
                style={{ '--o': `${s.cx}px 47px` } as CSSProperties}
                x1={s.cx}
                y1="49"
                x2={s.cx}
                y2="36.5"
              />
              <circle className="toggle-chess-clock__pin" cx={s.cx} cy="47" r="1.3" />
            </g>
          ))}
        </svg>
        <span>Black to move</span>
      </label>
    </>
  );
}

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

Install with shadcn

npx shadcn@latest add @movin/toggle-chess-clock
Source on GitHub

About this component

A whose-turn switch drawn as a chess clock: the plungers rock across, one clock stops dead and the other starts ticking from where it was, its flag standing up.

Dependencies
None
Difficulty
Intermediate
Triggers
click, state change, loop
Techniques
keyframes, css transition, svg
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

A native checkbox with role="switch" inside a <label>, so Space toggles it and the visible text names it; :focus-visible on the input outlines the drawing, which is aria-hidden. Under prefers-reduced-motion the hands stay still.

More toggles

See all
Drawbridge
Flytrap
Gate
Handbrake