When Timers Aren't Precise Enough

JavaScript developers often reach for timers when something needs to happen at a scheduled moment. But timers are inherently imprecise. Every task goes through a queue, and your callback only executes when the event loop gets to it. You can define a minimum delay, but you cannot predict what will already be in that queue.

This creates several problems for animation work:

  • Low precision. Self-adjusting timers that measure the gap between planned and actual execution can improve accuracy, but they fall short when ticks must be spaced by only a few milliseconds.
  • Pile ups. If background tabs get suspended, multiple timer callbacks can accumulate in the queue and fire all at once when the tab becomes active again.
  • Crowded queue. Libraries and frameworks already put plenty of code in the queue, making it more likely your animation callbacks land at an unfortunate moment.

For managing animations, requestAnimationFrame is a better choice than timers because it gives you rhythm — your code runs right before the user sees anything. But even that requires manual handling of timing calculations.

Animations Off the Queue

The Web Animations API takes a different approach. Instead of scheduling callbacks for each frame, you define animations that live on a shared timeline. All animations tied to the same timeline share an internal clock that starts at page load. That shared clock is what keeps elements in sync — no drifting, no "off-beat" elements.

The power comes from the startTime property. Measured in milliseconds from page load, it lets you pin an animation to an exact moment on the timeline. You can even specify fractional milliseconds, and browser settings permitting, that precision is honored.

More interesting still: negative start times are allowed. Set an animation's startTime to -1000, and it behaves as if it has already played for one second at page load. For the user, the animation appears to have started before the page even arrived.

Note: timeline and startTime are still experimental technologies.

A Clock That Never Drifts

A clock is the ultimate test of timing precision. When a clock hand must update exactly when the second ticks over, any timing error becomes visibly obvious.

An analog clock is straightforward with Web Animations API. Three hands, each performing an endless single rotation at different speeds:

  • The seconds hand completes a revolution in 60,000 milliseconds.
  • The minutes hand rotates 60 times slower.
  • The hours hand takes the same time as 24 minute-hand revolutions, matching a 24-hour dial.

Set every animation to the same startTime, and the hands stay in perfect sync. No queue management, no worry about suspended tabs — it is defined once and it holds.

A digital clock is trickier. Each digit is a container with an overflowing row of numbers from zero to one. Sitting inside the row are equal-width cells, and translating the row exposes the correct digit. This works as a series of animations, each running the duration of its digit's cycle.

Anchoring to Midnight

The interesting part is tying these looping animations to real time. The key is computing a shared starting point — midnight. The calculation looks like this:

const start_time = document.timeline.currentTime -
    (Date.now() - (Date.now() % (1000 * 60 * 60 * 24)) +
    (new Date().getTimezoneOffset() * 1000 * 60));

Breaking that down: strip full days from Date.now(), adjust for timezone offset, and you have the milliseconds elapsed since midnight. To place that into the document's timeline, subtract the current timeline time. Apply the resulting value to all animations with startTime, and every element behaves as if it has been running since the day started.

You could theoretically anchor animations to January 1, 1970 — making them appear to have run for decades. Some browsers impose undocumented limits on animation duration that would prevent this from working in practice, but the principle stands. Two clocks, one set to run since midnight and another since 1970, would both show the same time in perfect sync.

The key difference from the JavaScript timer approach: coordination comes from a shared definition of time rather than an effort to schedule callbacks. Each animation is merely playing along a timeline, and the timeline itself ensures they all move together.

Where Keyframes Fall Short

Defining motion through keyframes is elegant, but it is not a universal solution. Anything that does not naturally map to a keyframe cycle will require creative structuring. Animating a shadow's direction, for example, is awkward because shadows use x/y coordinates rather than an angle. To make the hands' shadows orbit properly, each hand was split into three separate elements: a rotating wrapper, the hand's body, and an independently animated shadow that copies the body's shape but has its own color and motion.

This decomposition adds elements, but it also adds flexibility. Since the wrapper already handles the primary rotation and timing, the additional shadow animation does not introduce sync complexity. The extra markup is a fair trade for the styling control it affords.

Handling Irregular Cycles

Not all time-based values fit into a clean loop. On the analog clock, each hand rotates once per hour, minute, or second — trivial keyframes. The digital clock's hours, however, do not follow a steady decimal or duodecimal cycle. The tens digit of the hour value is not regular: it jumps from 1 to 2 for only four hours before resetting. This required a wider digit element capable of counting from zero to ninety-nine. That wide digit was then reused for the date complication, simply by extending the animation duration from hours to days.

The calendar itself, however, refuses simplification. Months vary in length, and encoding the Gregorian calendar's rules into a single keyframe is not practical. Instead, the date complication pulls the current month's length from the Date object. The animation duration is set to the length of the current month, and the iterationStart property rewinds the animation to match today's date from a shared start time.

Rebuilding the date animation at each month boundary uses the finished promise of the current month's animation. This creates a small dependency on external state rather than a pure infinite loop. The slight imprecision at month rollover is an acceptable compromise given the underlying irregularity of the calendar.

Trade-Offs Worth Noting

Web Animations API shines when your target motion can be expressed as a small set of keyframes with a fixed duration. It keeps all animations on a single, drift-free timeline, and keyframe work is minimal. But tasks that require irregular intervals, dynamic boundaries, or non-trivial property mapping still need workarounds. In those cases, alternatives like requestAnimationFrame or performance.now() are available — though they require you to calculate interpolation manually.

The choice depends on how well your specific case fits the keyframe model. If the fit is close, the API removes a great deal of boilerplate and keeps everything synchronized. If the fit is poor, weigh the complexity of the workaround against writing the interpolation logic yourself. The clock demo illustrates both extremes: the hands required almost no effort, while the date logic needed more involved handling. Knowing where that boundary sits for your own interface is the practical takeaway.