Geneva drive
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>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-geneva" role="status" aria-label="Loading">
<svg viewBox="0 0 112 84" aria-hidden="true">
<g transform="translate(73 42)">
<g class="loader-geneva__cross">
<path class="loader-geneva__wheel" d="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"></path>
<circle class="loader-geneva__hub" r="4.5"></circle>
<circle class="loader-geneva__bolt" r="1.4"></circle>
</g>
</g>
<g transform="translate(33 42)">
<g class="loader-geneva__drive">
<path class="loader-geneva__lock" d="M11.647 -13.72A18 18 0 1 0 11.647 13.72A31.5 31.5 0 0 1 11.647 -13.72Z"></path>
<line class="loader-geneva__crank" x1="0" y1="0" x2="28.28" y2="0"></line>
<circle class="loader-geneva__pin" cx="28.28" cy="0" r="2.6"></circle>
<circle class="loader-geneva__hub" r="4"></circle>
<circle class="loader-geneva__bolt" r="1.3"></circle>
</g>
</g>
</svg>
</div>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; }
}Goal: Create a "Geneva drive" loader.
Visual behavior: 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.
Technique:
- The drive wheel turns at a constant rate. The cross wheel follows the true Geneva motion, sampled every 7.5 degrees of drive into keyframes: still, then a quarter turn that starts and ends at rest while the pin runs through the slot.
- The cross wheel outline is computed from its geometry, with slots for the pin and curved cut-outs that the drive's locking disc fits while it waits. Its four-fold symmetry hides the jump from -90 back to 0 degrees.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion both wheels are still.
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 "Geneva drive" loader.
Visual behavior: 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.
Technique:
- The drive wheel turns at a constant rate. The cross wheel follows the true Geneva motion, sampled every 7.5 degrees of drive into keyframes: still, then a quarter turn that starts and ends at rest while the pin runs through the slot.
- The cross wheel outline is computed from its geometry, with slots for the pin and curved cut-outs that the drive's locking disc fits while it waits. Its four-fold symmetry hides the jump from -90 back to 0 degrees.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion both wheels are still.
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 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.