Geneva drive

No dependencies
Code format
React + Tailwind
const css = `
  @keyframes loader-geneva-drive { from { transform: rotate(-90deg); } to { transform: rotate(270deg); } }
  /* The cross wheel's true Geneva profile, sampled every 7.5deg of the drive: still, then a quick
     quarter turn that starts and ends at rest while the pin runs through the slot. */
  @keyframes loader-geneva-cross {
    0%, 12.5% { transform: rotate(0deg); }
    14.583% { transform: rotate(-0.56deg); }
    16.667% { transform: rotate(-2.63deg); }
    18.75% { transform: rotate(-7.03deg); }
    20.833% { transform: rotate(-15deg); }
    22.917% { transform: rotate(-27.84deg); }
    25% { transform: rotate(-45deg); }
    27.083% { transform: rotate(-62.16deg); }
    29.167% { transform: rotate(-75deg); }
    31.25% { transform: rotate(-82.97deg); }
    33.333% { transform: rotate(-87.37deg); }
    35.417% { transform: rotate(-89.44deg); }
    37.5%, 100% { transform: rotate(-90deg); }
  }
  .loader-geneva { position: relative; width: 134px; height: 100px; }
  .loader-geneva svg { width: 100%; height: 100%; overflow: visible; }
  .loader-geneva__drive { animation: loader-geneva-drive 2.2s linear infinite; }
  .loader-geneva__cross { animation: loader-geneva-cross 2.2s linear infinite; }
  .loader-geneva__wheel { fill: light-dark(#cbced3, #393c41); stroke: light-dark(#5f626a, #a7abb2); stroke-width: 1; stroke-linejoin: round; }
  .loader-geneva__lock { fill: light-dark(#dbdde1, #2e3035); stroke: light-dark(#6d7179, #7d8087); stroke-width: 1; }
  .loader-geneva__crank { stroke: light-dark(#6d7179, #7d8087); stroke-width: 5; stroke-linecap: round; }
  .loader-geneva__pin { fill: light-dark(#42454c, #e5e6e9); stroke: light-dark(#dbdde1, #2e3035); stroke-width: 0.8; }
  .loader-geneva__hub { fill: light-dark(#f3f4f7, #1a1c1f); stroke: light-dark(#5f626a, #a7abb2); stroke-width: 1.4; }
  .loader-geneva__bolt { fill: light-dark(#5f626a, #a7abb2); }
  @media (prefers-reduced-motion: reduce) {
    .loader-geneva__drive, .loader-geneva__cross { animation: none; }
  }
`;

// The cross wheel: rim r31, a locking cut-out (r18.5, centred 40 away) facing every quarter,
// and a slot for the pin between each pair.
const CROSS =
  'M27.734 -13.849A18.5 18.5 0 0 0 27.734 13.849A31 31 0 0 1 23.939 19.696L8.839 4.596A3 3 0 0 0 4.596 8.839L19.696 23.939A31 31 0 0 1 13.849 27.734A18.5 18.5 0 0 0 -13.849 27.734A31 31 0 0 1 -19.696 23.939L-4.596 8.839A3 3 0 0 0 -8.839 4.596L-23.939 19.696A31 31 0 0 1 -27.734 13.849A18.5 18.5 0 0 0 -27.734 -13.849A31 31 0 0 1 -23.939 -19.696L-8.839 -4.596A3 3 0 0 0 -4.596 -8.839L-19.696 -23.939A31 31 0 0 1 -13.849 -27.734A18.5 18.5 0 0 0 13.849 -27.734A31 31 0 0 1 19.696 -23.939L4.596 -8.839A3 3 0 0 0 8.839 -4.596L23.939 -19.696A31 31 0 0 1 27.734 -13.849Z';
// The drive's locking disc, relieved on the pin side so the cross wheel can turn past it.
const LOCK = 'M11.647 -13.72A18 18 0 1 0 11.647 13.72A31.5 31.5 0 0 1 11.647 -13.72Z';

export function GenevaLoader() {
  return (
    <>
      <style>
        {css}
      </style>
      <div className="loader-geneva" role="status" aria-label="Loading">
        <svg viewBox="0 0 112 84" aria-hidden="true">
          <g transform="translate(73 42)">
            <g className="loader-geneva__cross">
              <path className="loader-geneva__wheel" d={CROSS} />
              <circle className="loader-geneva__hub" r="4.5" />
              <circle className="loader-geneva__bolt" r="1.4" />
            </g>
          </g>
          <g transform="translate(33 42)">
            <g className="loader-geneva__drive">
              <path className="loader-geneva__lock" d={LOCK} />
              <line className="loader-geneva__crank" x1="0" y1="0" x2="28.28" y2="0" />
              <circle className="loader-geneva__pin" cx="28.28" cy="0" r="2.6" />
              <circle className="loader-geneva__hub" r="4" />
              <circle className="loader-geneva__bolt" r="1.3" />
            </g>
          </g>
        </svg>
      </div>
    </>
  );
}

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

Install with shadcn

npx shadcn@latest add @movin/loader-geneva
Source on GitHub

About this component

A Geneva drive: the drive wheel turns steadily and its pin catches a slot once per turn, flicking the cross wheel a quarter turn and locking it again.

Dependencies
None
Difficulty
Advanced
Triggers
continuous, loop
Techniques
keyframes, svg, transform only
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion both wheels are still.

More loaders

See all
Hamster
Jellyfish
Juggle
Spirit level