The CSS Mechanical Clock

The clock on this site is a real, ticking timepiece — synced to your system clock, showing the correct time right now. But there's no clock library, no canvas rendering, and no image of a clock anywhere in the code. The gears, the face, the hands, and the brass finish are all produced by the browser's own styling and animation engine, from pure CSS and a small amount of JavaScript.


This page explains how it works, starting with the weirdest part: how CSS can draw a gear.

Want to see the clock full-screen? The standalone page puts it front and center with nothing else in the way — gears spinning, hands ticking in real time.

Open the interactive clock

How CSS Draws a Gear

CSS has a gradient function called repeating-conic-gradient. It paints a color pattern that repeats at a fixed angular interval around the center of an element. If you set the interval to exactly 1/N of a full circle, you get N identical wedge-shaped slices — which, if sized correctly, look exactly like gear teeth.


The tooth count is stored as a CSS custom property (--teeth), and the gradient references it directly:

/* A gear with 48 teeth */
.gear {
  /* Each tooth pitch = 360deg / 48 = 7.5deg             */
  /* Tooth fills 52% of the pitch, gap fills the rest    */
  background: repeating-conic-gradient(
    from 0deg,
    #d4a017  0deg  calc(360deg / var(--teeth) * 0.52),
    #0e0e12  calc(360deg / var(--teeth) * 0.52)  calc(360deg / var(--teeth))
  );
}

To turn that filled disc into an actual gear — with a hub, spokes, and an annular tooth ring rather than a full disc of teeth — multiple gradient layers are stacked in a single background property. CSS processes them top-to-bottom, and each layer can use radial-gradient with transparent inner/outer regions to create rings:


  • Layer 1 (top): Dark hub cap — a filled circle in the center
  • Layer 2: Brass hub ring — a thin brass annulus just outside the hub
  • Layer 3: Spoke plate — a wide brass annulus covering most of the gear body
  • Layer 4: Spoke cutouts — a repeating-conic-gradient that punches dark wedge-shaped gaps through the spoke plate, leaving only the spokes
  • Layer 5: Tooth annulus — a narrow brass ring at the outer edge
  • Layer 6 (bottom): Tooth serrations — the repeating-conic-gradient that creates the teeth

No images. No SVG. No canvas. Just one property with six gradient functions.

Gear Ratios as CSS Variables

In a real mechanical clock, the gear train follows a simple physical law: if gear A has TA teeth and meshes with gear B having TB teeth, then for every full rotation of A, gear B completes exactly TA / TB rotations. Larger gears turn slower; smaller gears turn faster.


CSS animations have an animation-duration property. By making each gear's duration proportional to its tooth count, the CSS itself encodes the gear ratio law:

/* Gear A is the anchor: 48 teeth, one rotation per 120s */
/* All other durations are derived from the same ratio   */

#gA { animation: rotateCW  120s linear infinite; } /* 48T */
#gB { animation: rotateCCW  75s linear infinite; } /* 30T — 120×(30÷48) */
#gC { animation: rotateCW   50s linear infinite; } /* 20T — 120×(20÷48) */
#gD { animation: rotateCCW  85s linear infinite; } /* 34T — 120×(34÷48) */
#gE { animation: rotateCW   60s linear infinite; } /* 24T — 120×(24÷48) */
#gF { animation: rotateCCW 100s linear infinite; } /* 40T — 120×(40÷48) */

Meshing gears also alternate direction — CW then CCW then CW — because a gear pushes its neighbour backward. That's why the animations alternate between rotateCW and rotateCCW.


The tooth count is also used to draw the teeth themselves (via the conic gradient pitch), so the visual density of teeth and the rotation speed are always mechanically consistent with each other. A gear that rotates twice as fast has half as many teeth, and those teeth are visually wider-pitched to match.

The animation-delay Time-Sync Trick

CSS animations have an often-overlooked feature: animation-delay accepts negative values. When you write animation-delay: -30s, the browser acts as if the animation started 30 seconds ago — so it immediately renders the frame that would have played at the 30-second mark.


This is the key to syncing the hands to real time. On page load, a few lines of JavaScript read the current time and compute how far into each hand's rotation cycle the current moment falls. That offset becomes a negative delay:

const now = new Date();
const h  = now.getHours() % 12;
const m  = now.getMinutes();
const s  = now.getSeconds();
const ms = now.getMilliseconds();

/* How many seconds into each hand's full cycle are we? */
const secOffset  = s  + ms / 1000;           // 0–60s    (60s cycle)
const minOffset  = m  * 60  + secOffset;     // 0–3600s  (3600s cycle)
const hourOffset = h  * 3600 + minOffset;    // 0–43200s (43200s cycle)

/* Seek each CSS animation to the correct position */
hourHand.style.animationDelay   = `-${hourOffset}s`;
minuteHand.style.animationDelay = `-${minOffset}s`;
secondHand.style.animationDelay = `-${secOffset}s`;

Once the delay is set, the browser takes over completely. There's no setInterval, no requestAnimationFrame loop, no JavaScript touching the clock after the initial sync. The hands tick forward on their own, driven entirely by the CSS animation engine — accurate to the millisecond on load, and smooth at 60 frames per second thereafter.

Why CSS Animations Are Butter-Smooth

CSS transform animations — rotating, scaling, translating — are handled by the browser's compositor thread, which runs separately from the main JavaScript thread. This means they continue at 60 fps (or higher, on high-refresh displays) even when JavaScript is busy doing other work: running a garbage collector, parsing a large JSON response, or updating the DOM.


A setInterval-based clock would jitter whenever the JS thread was occupied. These CSS-animated hands don't. The six spinning gears and the three ticking hands all run concurrently on GPU-accelerated layers with no interference from each other or from anything else happening on the page.

The Clock Face in SVG

While the gears are pure CSS, the clock face is an inline SVG. The bezel, the dark dial, the numerals, and the tick marks are all vector graphics defined in code. The 60 tick marks and 12 Roman numerals are placed by JavaScript using basic trigonometry — computing the x and y positions along a circle and inserting the SVG elements dynamically, rather than writing out 72 lines of coordinates by hand:

/* Place each numeral at radius r from center (200, 200) */
labels.forEach((label, i) => {
  const angle = (i * 30 - 90) * Math.PI / 180; // 30deg per hour, 12 o'clock = -90deg
  const x = 200 + r * Math.cos(angle);
  const y = 200 + r * Math.sin(angle);
  // create SVG <text> element at (x, y) ...
});

The clock hands themselves are SVG <rect> elements (rounded rectangles) grouped inside <g> tags with transform-origin set to the clock center. The CSS animation rotates the entire group around that origin — so the hand and its counterweight both rotate together, just like the arbor of a real watch movement.


SVG <filter> elements add the luminous glow around the hands: a Gaussian blur layer is merged back with the original, creating a soft halo. The second hand's red glow is amplified through a color matrix filter before merging, making it distinctly warm while the hour and minute hands glow cooler white.

Why I Built It

I'm drawn to projects that have an obvious analog in the physical world but an unexpected digital implementation. A mechanical clock is one of humanity's oldest precision instruments. Rendering one entirely from browser styling felt like a good puzzle: how much of the feel of a watchmaker's craft can you express through CSS variables and gradient math?


The answer turned out to be more than you'd expect. The gear ratio law, the alternating mesh directions, the tooth pitch, the spoke geometry: all of it has a direct CSS counterpart. The code doesn't simulate a clock; in a meaningful sense, it is a clock, built from the same principles, just in a different medium.