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 clockHow 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-gradientthat 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-gradientthat 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.