Of Course We Can Make a CSS-Only Clock That Tells the Current Time!

Let’s build a fully functioning and settable “analog” clock with CSS custom properties and the calc() function. Then we’ll convert it into a “digital” clock as well. All this with no JavaScript!

Here’s a quick look at the clocks we’ll make:

Brushing up on the calc() function

CSS preprocessors teased us forever with the ability to calculate numerical CSS values. The problem with pre-processors is that they lack knowledge of the context after the CSS code has compiled. This is why it’s impossible to say you want your element width to be 100% of the container minus 50 pixels. This produces an error in a preprocessor:

width: 100% - 50px;
// error: Incompatible units: 'px' and '%'

Preprocessors, as their name suggests, preprocess your instructions, but their output is still just plain old CSS which is why they can’t reconcile different units in your arithmetic operations. Ana has gone into great detail on the conflicts between Sass and CSS features.

The good news is that native CSS calculations are not only possible, but we can even combine different units, like pixels and percentages with the calc() function:

width: calc(100% - 50px);

calc() can be used anywhere a length, frequency, angle, time, percentage, number, or integer is allowed. Check out the CSS Guide to CSS Functions for a complete overview.

What makes this even more powerful is the fact that you can combine calc() with CSS custom properties—something preprocessors are unable to handle.

The hands of the clock

Image of a round clock with a light grey background. The hours hand is dark blue pointed at 2, the minutes are light blue pointed at 9 and the seconds are yellow and pointed at 2.

Let’s lay out the foundations first with a few custom properties and the animation definition for the hands of the analog clock:

:root { --second: 1s; --minute: calc(var(--second) * 60); --hour: calc(var(--minute) * 60);
}
@keyframes rotate { from { transform: rotate(0); } to { transform: rotate(1turn); }
}

Everything starts in the root context with the --second custom property where we defined that a second should be, well, one second (1s). All future values and timings will be derived from this.

This property is essentially the heart of our clock and controls how fast or slow all of the clock’s hands go. Setting --second to 1s makes the clock match real-life time but we could make it go at half speed by setting it to 2s, or even 100 times faster by setting it to 10ms.

The first property we are calculating is the --minute hand, which we want equal to 60 times one second. We can reference the value from the --second property and multiply it by 60 with the help of calc() :

--minute: calc(var(--second) * 60);

The --hour hand property is defined using the exact same principle but multiplied by the --minute hand value:

--hour: calc(var(--minute) * 60);

We want all three hands on the clock to rotate from 0 to 360 degrees—around the shape of the clock face! The difference between the three animations is how long it takes each to go all the way around. Instead of using 360deg as our full-circle value, we can use the perfectly valid CSS value of 1turn.

@keyframes rotate { from { transform: rotate(0); } to { transform: rotate(1turn); }
}

These @keyframes simply tell the browser to turn the element around once during the animation. We have defined an animation named rotate and it is now ready to be assigned to the clock’s second hand:

.second.hand { animation: rotate steps(60) var(--minute) infinite;
}

We’re using the animation shorthand property to define the details of the animation. We added the name of the animation (rotate), how long we want the animation to run (var(--minute), or 60 seconds) and how many times to run it (infinite, meaning it never stops running). steps(60) is the animation timing function which tells the browser to perform the 1-turn animation in 60 equal steps. This way, the seconds hand ticks at each second rather than rotating smoothly along the circle.

While we are discussing CSS animations, we can define an animation delay (animation-delay) if we want the animation to start later, and we can change whether the animation should play forwards or backwards using animation-direction. We can even pause and restart the animations with animation-play-state.

The animation on the minute and hour hands will work very much like on the second hand. The difference is that multiple steps are unnecessary here—these hands can rotate in a smooth, linear fashion.

The minute hand takes one hour to complete one full turn, so:

.minute.hand { animation: rotate linear var(--hour) infinite;
}

On the other hand (pun intended) the hour hand takes twelve hours to go around the clock. We don’t have a separate custom property for this amount of time, like --half-day, so we will multiply --hour by twelve:

.hour.hand { animation: rotate linear calc(var(--hour) * 12) infinite;
}

You probably get the idea of how the hands of the clock work by now. But it would not be a complete example if we didn’t actually build the clock.

The clock face

So far, we’ve only looked at the CSS aspect of the clock. We also need some HTML for all that to work. Here’s what I’m using:

<main> <div class="clock"> <div class="second hand"></div> <div class="minute hand"></div> <div class="hour hand"></div> </div>
</main>

Let’s see what we have to do to style our clock:

.clock { width: 300px; height: 300px; border-radius: 50%; background-color: var(--grey); margin: 0 auto; position: relative;
}

We made the clock 300px tall and wide, made the background color grey (using a custom property, --grey, we can define later) and turned it into a circle with a 50% border radius.

There are three hands on the clock. Let’s first move these to the center of the clock face with absolute positioning:

.hand { position: absolute; left: 50%; top: 50%;
}

Notice the name of this class (.hands) because all three hands use it for their base styles. That’s important because any changes to this class are applied to all three hands.

Let’s define the hand dimensions and color things up:

.hand { position: absolute; left: 50%; top: 50%; width: 10px; height: 150px; background-color: var(--blue);
}

The hands are now all in place:

A round clock with just one light blue hand pointing directly at 6.

Getting proper rotation

Let’s hold off celebrating just a moment. We have a few issues and they might not be obvious when the clock hands are this thin. What if we change them to be 100px wide:

A round clock with a light grey background. The light blue hand is thick and still pointed at 6, but

We can now see that if an element is positioned 50% from the left, it is aligned to the center of the parent element—but that’s not exactly what we need. To fix this, the left coordinate needs to be 50%, minus half the width of the hand, which is 50px in our case:

Grey circle with a red vertical line bisecting it in the center. Two red arrows are on the clock, one labeled 50% pointing at the red line and another labeled negative 50 pixels pointing away from the red line.

Working with multiple different measurements is a breeze for the calc() function:

.hand { position: absolute; left: calc(50% - 50px); top: 50%; width: 100px; height: 150px; background-color: var(--grey);
}

This fixes our initial positioning, however, if we try to rotate the element we can see that the transform origin, which is the pivot point of the rotation, is at the center of the element:

Grey circle showing a blue rectangle on top rotated counter-clockwise.

We can use the transform-origin property to change the rotation origin point to be at the center of the x-axis and at the top on the y-axis:

.hand { position: absolute; left: calc(50% - 50px); top: 50%; width: 100px; height: 150px; background-color: var(--grey); transform-origin: center 0;
}

This is great, but not perfect because our pixel values for the clock hands are hardcoded. What if we want our hands to have different widths and heights, and scale with the actual size of the clock? Yes, you guessed right: we need a few CSS custom properties!

.second { --width: 5px; --height: 140px; --color: var(--yellow);
}
.minute { --width: 10px; --height: 90px; --color: var(--blue);
}
.hour { --width: 10px; --height: 50px; --color: var(--dark-blue);
}

With this, we’ve defined custom properties for the individual hands. What’s interesting here is that we gave these properties the same names: --width, --height, and --color. How is it possible that we gave them different values but they don’t overwrite each other? And which value will I get back if I call var(--width), var(--height) or var(--color)?

Let’s look at the hour hand:

<div class="hour hand"></div>

We assigned new custom properties to the .hour class and they are locally scoped to the element, which includes the element and all its children. This means any CSS style applied to the element—or its children accessing the custom properties—will see and use the specific values that were set within their own scope. So if you call var(--width) inside the hour hand element or any ancestor elements inside that, the value returned from our example above is 10px. This also means that if we try using any of these properties outside these elements—for example inside the body element—they are inaccessible to those elements outside the scope.

Moving on to the hands for seconds and minutes, we enter a different scope. That means the custom properties can be redefined with different values.

.second { --width: 5px; --height: 140px; --color: var(--yellow);
}
.minute { --width: 10px; --height: 90px; --color: var(--blue);
}
.hour { --width: 10px; --height: 50px; --color: var(--dark-blue);
}
.hand { position: absolute; top: 50%; left: calc(50% - var(--width) / 2); width: var(--width); height: var(--height); background-color: var(--color); transform-origin: center 0;
}

The great thing about this is that the .hand class (which we assigned to all three hand elements) can reference these properties for the calculations and declarations. And each hand will receive its own properties from its own scope. In a way, we’re personalizing the .hand class for each element to avoid unnecessary repetition in our code.

Our clock is up and running and all hands are moving at the correct speed:

The finished clock. Grey background, with minutes and hours pointing to 6 and seconds pointing to 7.

We could stop here but let me suggest a few improvements. The hands on the clock start at 6 o’clock, but we could set their initial positions to 12 o’clock by rotating the clock 180 degrees. Let’s add this line to the .clock class:

.clock { /* same as before */ transform: rotate(180deg);
}

The hands might look nice with rounded edges:

.hand { /* same as before */ border-radius: calc(var(--width) / 2);
}

Our clock looks and works great! And all hands start from 12 o’clock, exactly how we want it!

Setting the clock

Even with all these awesome features, the clock is unusable as it fails terribly at telling the actual time. However, there are some hard limitations to what we can do about this. It’s simply not possible to access the local time with HTML and CSS to automatically set our clock. But we can prepare it for manual setup.

We can set the clock to start at a certain hour and minute and if we run the HTML at exactly that time it will keep the time accurately afterwards. This is basically how you set a real-world clock, so I think this is an acceptable solution. 😅

Let’s add the time we want to start the clock as custom properties inside the .clock class:

.clock { --setTimeHour: 16; --setTimeMinute: 20; /* same as before */
}

The current time for me as I write is coming up to 16:20 (or 4:20) so the clock will be set to that time. All I need to do is refresh the page at 16:20 and it will keep the time accurately.

OK, but… how can we set the time to these positions and rotate the hands if a CSS animation is already controlling the rotation?

Ideally, we want to rotate and set the hands of the clock to a specific position when the animation starts at the very beginning. Say you want to set the hour hand to 90 degrees so it starts at 3:00 pm and initialize the rotation animation from this position:

/* this will not work */
.hour.hand { transform: rotate(90deg); animation: rotate linear var(--hour) infinite;
}

Well, unfortunately, this will not work because the transform is immediately overridden by the animation, as it modifies the very same transform property. So, no matter what we set this to, it will go back to 0 degrees where the first keyframe of the animation starts.

We can set the rotation of the hand independently from the animation. For example, we could wrap the hand into an extra element. This extra parent element, the “setter,” would be responsible for setting the initial position, then the hand element inside could animate from 0 to 360 degrees independently. The starting 0-degree position would then be relative to what we set the parent setter element to.

This would work but luckily there’s a better option! Let me amaze you! 🪄✨✨

The animation-delay property is what we usually use to start the animation with some predefined delay.

The trick is to use a negative value, which starts the animation immediately, but from a specific point in the animation timeline!

To start 10 seconds into the animation:

animation-delay: -10s;

In case of the hour and minute hands, the actual value we need for the delay is calculated from the --setTimeHour and --setTimeMinute properties. To help with the calculation, let’s create two new properties with the amount of time shifting we need:

  • The hour hand needs to be the hour we want to set the clock, multiplied by an hour.
  • The minute hand shifts the minute we want to set the clock multiplied by a minute.
--setTimeHour: 16;
--setTimeMinute: 20;
--timeShiftHour: calc(var(--setTimeHour) * var(--hour));
--timeShiftMinute: calc(var(--setTimeMinute) * var(--minute));

These new properties contain the exact amount of time we need for the animation-delay property to set our clock. Let’s add these to our animation definitions:

.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);
}

Notice how we multiplied these values by -1 to convert them to a negative number.

We have almost reached perfection with this, but there’s a slight issue: if we set the number of minutes to 30, for example, the hour hand needs to already be halfway through to the next hour. An even worse situation would be to set the minutes to 59 and the hour hand is still at the beginning of the hour.

fix this, all we need to do is add the minute shift and the hour shift values together for the hour hand:

.hour.hand { animation: rotate linear calc(var(--hour) * 12) infinite; animation-delay: calc( (var(--timeShiftHour) + var(--timeShiftMinute)) * -1 );
}

And I think with this we have fixed everything. Let’s admire our beautiful, pure CSS, settable, analog clock:

Let’s go digital

In principle, an analog and a digital clock both use the same calculations, the difference being the visual representation of the calculations.

Clock with a rectangular light grey background with the numeric time reading 14 45 18 in 24-hour format.

Here’s my idea: we can create a digital clock by setting up tall, vertical columns of numbers and animate these instead of rotating the clock hands. Removing the overflow mask from the final version of the clock container reveals the trick:

The new HTML markup needs to contain all the numbers for all three sections of the clock from 00 to 59 on the second and minute sections and 00 to 23 on the hour section:

<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>

To make these numbers line up, we need to write some CSS, but to get started with the styling we can copy over the custom properties from the :root scope of the analog clock straight away, as time is universal:

:root { --second: 1s; --minute: calc(var(--second) * 60); --hour: calc(var(--minute) * 60);
}

The outermost wrapper element, the .clock, still has the very same custom properties for setting the initial time. All that’s changed is that it becomes a flexbox container:

.clock { --setTimeHour: 14; --setTimeMinute: 01; --timeShiftHour: calc(var(--setTimeHour) * var(--hour)); --timeShiftMinute: calc(var(--setTimeMinute) * var(--minute)); width: 150px; height: 50px; background-color: var(--grey); margin: 0 auto; position: relative; display: flex;
}

The three unordered lists and the list items inside them that hold the numbers don’t need any special treatment. The numbers will stack on top of each other if their horizontal space is limited. Let’s just make sure that there is no list styling to prevent bullet points and that we center things for consistent placement:

.section > ul { list-style: none; margin: 0; padding: 0;
}
.section > ul > li { width: 50px; height: 50px; font-size: 32px; text-align: center; padding-top: 2px;
}

The layout is done!

Outside each unordered list is a <div> with a .section class. This is the element that wraps each section, so we can use it as a mask to hide the numbers that fall outside the visible area of the clock:

.section { position: relative; width: calc(100% / 3); overflow: hidden;
}

The structure of the clock is done and the rails are now ready to be animated.

Animating the digital clock

The basic idea behind the whole animation is that the three strips of numbers can be moved up and down within their masks to show different numbers from 0 to 59 for seconds and minutes, and 0 to 23 for hours (for a 24-hour format).

We can do this by changing the translateY transition function in CSS for the individual strips of numbers from 0 to -100%. This is because 100% on the y-axis represents the height of the whole strip. A value of 0% will show the first number, and 100% will show the last number of the current strip.

Previously, our animation was based on rotating a hand from 0 to 360 degrees. We now have a different animation that moves the number strips from 0 to -100% on the y-axis:

@keyframes tick { from { transform: translateY(0); } to { transform: translateY(-100%); }
}

Applying this animation to the seconds number strip can be done the same way as the analog clock. The only difference is the selector and the name of the animation that’s referenced:

.second > ul { animation: tick steps(60) var(--minute) infinite;
}

With the step(60) setting, the animation ticks between numbers like we did for the second hand on the analog clock. We could change this to ease and then the numbers would smoothly slide up and down as if they were on a ribbon of paper.

Assigning the new tick animation to the minute and hour sections is just as straightforward:

.minute > ul { animation: tick steps(60) var(--hour) infinite; animation-delay: calc(var(--timeShiftMinute) * -1);
}
.hour > ul { animation: tick steps(24) calc(24 * var(--hour)) infinite; animation-delay: calc(var(--timeShiftHour) * -1);
}

Again, the declarations are very similar, what’s different this time is the selector, the timing function, and the animation name.

The clock now ticks and keeps the correct time:

Time reading 14 1 10 in 24-hour format.

One more detail: The blinking colon (:)

Again we could stop here and call it a day. But there’s one last thing we can do to make our digital clock a little more realistic: make the colon separator between the minutes and seconds blink as each second passes.

We could add these colons in the HTML but they are not part of the content. We want them to be an enhancement to the appearance and style of the clock, so CSS is the right place to store this content. That’s what the content property is for and we can use it on the ::after pseudo-elements for the minutes and hours:

.minute::after,
.hour::after { content: ":"; margin-left: 2px; position: absolute; top: 6px; left: 44px; font-size: 24px;
}

That was easy! But how can we make the seconds colon blink too? We want it animated so we need to define a new animation? There are many ways to achieve this but I thought we should change the content property this time to demonstrate that, quite unexpectedly, it is possible to change its value during an animation:

@keyframes blink { from { content: ":"; } to { content: ""; }
}

Animating the content property is not going to work in every browser, so you could just change that to opacity or visibility as a safe option…

The final step is to assign the blink animation to the pseudo-element of the minute section:

.minute::after { animation: blink var(--second) infinite;
}

And with that, we are all done! The digital clock keeps the time accurately and we even managed to add a blinking separator between the numbers.

Book: All you need is HTML and CSS

This is just one example project from my new book, All you need is HTML and CSS. It’s available on Amazon in both the U.S and U.K.

If you are just getting started with web development, the ideas in the book will help you level up and build interactive, animated web interfaces without touching any JavaScript.

If you are a seasoned JavaScript developer, the book is a good reminder that many things can be built with HTML and CSS alone, especially now that we have a lot more powerful CSS tools and features, like the ones we covered in this article. There are many examples in the book pushing the limits including interactive carousels, accordions, calculating, counting, advanced input validation, state management, dismissible modal windows, and reacting to mouse and keyboard inputs. There’s even a fully working star rating widget and a shopping basket.

Thanks for spending the time to build clocks with me!


The post Of Course We Can Make a CSS-Only Clock That Tells the Current Time! appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.