The calc() Function vs. Preprocessors
For years, CSS preprocessors offered the promise of dynamic values, but their static compilation means they can’t reconcile different units—like percentages and pixels—in a single operation. Native CSS calc() solves this by letting you combine arbitrary unit types on the fly, and it pairs even better with CSS custom properties, something preprocessors still can’t touch. You can use calc() wherever a length, frequency, angle, time, percentage, number, or integer is permitted.
width: calc(100% - 50px);
Building the Analog Clock Hands
Everything revolves around one custom property in the :root: --second, set to 1s. From this single definition, all timings flow. Change it to 2s and the clock runs at half speed; set it to 10ms and it runs 100 times faster.
:root {
--second: 1s;
--minute: calc(var(--second) * 60);
--hour: calc(var(--minute) * 60);
}
@keyframes rotate {
from { transform: rotate(0); }
to { transform: rotate(1turn); }
}
The other hand durations are derived mathematically. A minute is 60 seconds, and an hour is 60 minutes, so we use calc() to multiply those values:
--minute: calc(var(--second) * 60);
--hour: calc(var(--minute) * 60);
Animation with Steps for the Second Hand
Each hand rotates once—the CSS unit 1turn is perfect for this—but the second hand shouldn’t sweep smoothly. We want it to tick once per second, which is precisely what the steps(60) timing function does over a 60-second duration. Setting the correct animation to the second hand looks like this:
.second.hand {
animation: rotate steps(60) var(--minute) infinite;
}
Minute and hour hands rotate linearly because they don’t need the stepped movement. The minute hand takes 60 seconds—wait, that’s a full hour, so its animation duration is derived from the --minute property. The hour hand’s full cycle is twelve hours, so we multiply --hour by 12.
.minute.hand {
animation: rotate linear var(--hour) infinite;
}
.hour.hand {
animation: rotate linear calc(var(--hour) * 12) infinite;
}
Assembling the Clock Face
The HTML structure is straightforward, requiring a container for the clock and separate elements for each hand. Styling starts with basic shape and sizing:
.clock {
width: 300px;
height: 300px;
border-radius: 50%;
background-color: var(--grey);
margin: 0 auto;
position: relative;
}
Hands are centered with absolute positioning. However, aligning an element’s left edge to 50% places its edge, not its center, at the middle. That mismatch requires a calc() adjustment to subtract half the hand’s width. Positioning with transform-origin then moves the rotation pivot to the top-center of each hand, which is the correct point for a clock hand.
.hand {
position: absolute;
left: calc(50% - 50px);
top: 50%;
width: 100px;
height: 150px;
background-color: var(--grey);
}
.hand {
position: absolute;
left: calc(50% - 50px);
top: 50%;
width: 100px;
height: 150px;
background-color: var(--grey);
transform-origin: center 0;
}
Rather than hardcoding hand dimensions, custom properties can be scoped locally—right on each hand’s class. The .hand base class references those variables, and each hand instance gets values from its own scope. This avoids repetition and keeps the styling flexible.
<div class="hour hand"></div>
Improvements and Manual Setup
With animations running continuously, the hands start at 6 o’clock. Rotating the clock container by 180 degrees (transform: rotate(180deg)) resets the starting position to 12 o’clock. Adding border-radius makes the hands look smoother.
One unavoidable flaw remains: pure CSS can’t fetch local time. The clock can only be “set” manually. We can define custom properties for the initial hour and minute, say 16 and 20, and then translate those into an animation delay that starts the clock at that exact time.
An initial transform on the hand won’t work because the animation overrides it immediately. The trick is the negative animation-delay. A negative value starts the animation right away, but from a point in the timeline specified by that delay. For the hour hand, we want the delay to account for the hour value plus a partial shift based on the minutes, so the hand is properly positioned between hours. Calculating those time shifts looks like this:
--setTimeHour: 16;
--setTimeMinute: 20;
--timeShiftHour: calc(var(--setTimeHour) * var(--hour));
--timeShiftMinute: calc(var(--setTimeMinute) * var(--minute));
The calculations are then incorporated into the animation-delay declarations for the hands, multiplied by -1 to make them negative.
.second.hand {
animation: rotate steps(60) var(--minute) infinite;
}
.minute.hand {
animation: rotate linear var(--hour) infinite;
animation-delay: calc(var(--timeShiftMinute) * -1);
}
.hour.hand {
animation: rotate linear calc(var(--hour) * 12) infinite;
animation-delay: calc(var(--timeShiftHour) * -1);
}
.hour.hand {
animation: rotate linear calc(var(--hour) * 12) infinite;
animation-delay: calc(
(var(--timeShiftHour) + var(--timeShiftMinute)) * -1
);
}
A Digital Take on the Same Logic
The digital clock shares the same mechanics but changes the presentation. Instead of rotating hands, we slide tall vertical strips of numbers. Each section—hours 00-23, minutes 00-59, seconds 00-59—contains all its numbers stacked in a list. The container acts as a mask hiding anything outside the visible area.
<main>
<div class="clock">
<div class="hour section">
<ul>
<li>00</li>
<li>01</li>
<!-- etc. -->
<li>22</li>
<li>23</li>
</ul>
</div>
<div class="minute section">
<ul>
<li>00</li>
<li>01</li>
<!-- etc. -->
<li>58</li>
<li>59</li>
</ul>
</div>
<div class="second section">
<ul>
<li>00</li>
<li>01</li>
<!-- etc. -->
<li>58</li>
<li>59</li>
</ul>
</div>
</div>
</main>
Copying the timing custom properties from the analog clock’s :root makes perfect sense since time is universal. The main containers become flexbox layouts for orderly sections, and list items are cleared of default bullet styling and numbers are centered.
The animation shifts each number strip via translateY from 0 to -100%—where 100% equals the full height of the strip—showing the appropriate numbers sequentially. Setting the seconds strip to use steps(60) reproduces the ticking effect. We can swap the timing function to ease for a smooth “ribbon” scrolling effect if preferred. Minute and hour strips use analogous animations without the stepped timing.

The Blinking Separator
To mimic real digital clocks, the separator colon between sections should blink. CSS pseudo-elements are ideal for the non-content markup. With the content property, that styling can be set and, in some browsers, even animated to fade in and out.
@keyframes blink {
from { content: ":"; }
to { content: ""; }
}
If animating content isn’t supported in a target browser, switching to opacity or visibility is a safe fallback. The blink animation is then assigned to the minute section’s ::after pseudo-element. While it heavily relies on cutting-edge CSS features, this particular project does run continuously and keeps time accurately after the manual initial set.



