June 08, 2025

Digital Horology

I started getting ads for Klydoclock on Instagram. The product looks really neat:

I love the idea of a digital pendulum clock. I figured it would make for a cool DIY project: implement the clock display, then run it on a cheap tablet I can stick in a box. Of course, the Klydo folks build a cool product, working with multiple artists for the various clock faces, and, from what I can tell based on the videos, it's a polished product. I'm not trying to dunk on their hard work. I was simply intrigued by how one would go about implementing a pendulum clock.

I spent a couple of days playing with how I would implement something like this. Here's what I have so far:

This was built with a little bit of HTML, some CSS, and a dash of JavaScript. I have a love-hate relationship with CSS. From centering a div, to debugging obscure layout issues, I always think of this GIF:

CSS

That said, it's incredible how much one can achieve with CSS. In this blog post, I'll go over the clock implementation. I'll show some inline renderings of the clock, tweaked to be embedded inside this post. The original, unmodified source code is at https://github.com/vladris/vladris.github.io/tree/master/clock, and live at https://github.com/vladris/clock/. For example, a notable difference in the original implementation is sizes are specified in vh units, as I built it for full-screen rendering, but for the clocks embedded in this post the CSS uses px units. The code snippets are taken from the original implementation, so view source won't be as helpful. Now let's build a clock!

Pendulum

The most notable feature of the clock is the pendulum. I implemented this as 4 divs:

<div id="bottom-circle" class="circle">
    <div class="pendulum">
        <div class="pendulum-line"></div>
        <div class="pendulum-circle"></div>
    </div>
</div>

The circle class is just to make the div render as a circle, which we can do with CSS like this:

.circle {
    border-radius: 50%;
}

The bottom-circle further configures this:

#bottom-circle {
    width: 25vh;
    height: 25vh;
    position: relative;
    overflow: hidden;
}

We want a set height and width for it, and we want anything that ends up outside of the circle to be hidden.

Now here comes the fun part: the pendulum itself. The pendulum consists of a line and a circle, wrapped inside a container div:

.pendulum {
    position: absolute;
    top: -21vh;
    left: 50%;
    height: 44.5vh;
    transform: translateX(-50%);
}

.pendulum-circle {
    width: 22vh;
    height: 22vh;
    border-radius: 50%;
    box-shadow: 0 10px 20px rgba(0 0 0 / 50%);
    position: absolute;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
}

.pendulum-line {
    width: 2vh;
    height: 35vh;
    top: 0;
    left: 50%;
    position: absolute;
    transform: translateX(-50%);
}

The divs have left: 50% and transform: translateX(-50%). This centers them inside their container. The box-shadow adds a nice shadow effect. Here's what we have so far:

And here comes the swing animation:

.pendulum {
    ...

    animation: swing 2s infinite ease-in-out;
    transform: translateX(-50%);
}

@keyframes swing {
    0% {
        transform: rotate(-5deg);
    }
    50% {
        transform: rotate(5deg);
    }
    100% {
        transform: rotate(-5deg);
    }
}

This applies a swing animation to the pendulum, which rotates the whole thing by 5 degrees on each side:

The trick to it is the pendulum top is high outside the circle. The animation is anchored there (transform-origin: top) and the motion itself is only 5 degrees each way.

Clock face

The top part of the clock looks like this:

<div id="top-circle" class="circle">
    <div class="clock">
        <div class="hand" id="hour-hand"></div>
        <div class="hand" id="minute-hand"></div>
        <div class="pin"></div>
    </div>
</div>

The classic typeface is called Breguet, named after the luxury watch company that introduced it. Luckily, I found a free SVG with a Breguet dial:

Breguet

We can use this as the background of the top circle:

#top-circle {
    background-size: cover;
    background-image: url("breguet.svg");
}

The nice thing about SVGs is they scale very smoothly.

We need hour and minute hands, and also a pin in the middle. We already saw these in the clock top HTML:

<div id="top-circle" class="circle">
    <div class="clock">
        <div class="hand" id="hour-hand"></div>
        <div class="hand" id="minute-hand"></div>
        <div class="pin"></div>
    </div>
</div>

The styling for the clock face:

#top-circle {
    width: 50vh;
    height: 50vh;
    margin-bottom: 5vh;
    position: relative;
    background-size: cover;
    background-image: url("breguet.svg");
}

.clock {
    position: absolute;
    width: 100%;
    height: 100%;
    border-radius: 50%;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

.pin {
    width: 0.5vh;
    height: 0.5vh;
    border-radius: 50%;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

.hand {
    position: absolute;
    box-shadow: 0 0 10px rgba(0 0 0 / 50%);
    stroke-width: 100;
    top: 50%;
    left: 50%;
    border-radius: 1vh;
    transform: translate(-50%, -15%);
}

#hour-hand {
    height: 17vh;
    width: 1.8vh;
    transform-origin: 50% 15%;
}

#minute-hand {
    height: 22vh;
    width: 1.2vh;
    transform-origin: 50% 15%;
}

This looks like a lot but it really isn't: we center the clock with left: 50%; top: 50%; transform: translate(-50%, -50%). We do the same with the pin, this is just a small circle at the very center of the clock.

The hour and minute hands are similarly centered, with a slightly different translate: translate(-50%, -15%) - we want them to look like the pin goes through them at 15% of their length. We also add shadows to the hands.

Almost done! The only thing we need to do is show the time. This is where the bit of JavaScript comes in:

function updateClock() {
    const now = new Date();
    const minutes = now.getMinutes();
    const hours = now.getHours();

    const minuteDegree = (minutes / 60) * 360 + (seconds / 60) * 6 + 180;
    const hourDegree = (hours / 12) * 360 + (minutes / 60) * 30 + 180;

    document.getElementById(
        "minute-hand"
    ).style.transform = `translate(-50%, -15%) rotate(${minuteDegree}deg)`;
    document.getElementById(
        "hour-hand"
    ).style.transform = `translate(-50%, -15%) rotate(${hourDegree}deg)`;
}

setInterval(updateClock, 1000);
updateClock();

We implement an updateClock() function that gets the current time and rotates the hands. We compute how many degrees we want the minute hand and the hour hand to be rotated by:

const minuteDegree = (minutes / 60) * 360 + (seconds / 60) * 6 + 180;
const hourDegree = (hours / 12) * 360 + (minutes / 60) * 30 + 180;

Note the minute hand also takes into account the seconds while the hour hand takes into account the minutes, to ensure smooth movement.

We then get the minute-hand and hour-hand elements and update their transform to include a rotate by the determined number of degrees.

We use setInterval() to run updateClock every second. And that's it!

Skins

Since all the styling is done with CSS, it's very easy to provide multiple skins for it, much like Klydo does. Here's a Mickey Mouse version:

In fact, in my original implementation I have several clock faces, each coming with a set of color customizations and a background image for the clock. The following JavaScript looks for a style query parameter and loads the specified CSS file:

function loadCSSFromQueryParam() {
    const urlParams = new URLSearchParams(window.location.search);
    const style = urlParams.get("style") ?? "breguet";
    const styleParam = `${style}/style.css`;
    if (styleParam) {
        const link = document.createElement("link");
        link.rel = "stylesheet";
        link.href = styleParam;
        document.head.appendChild(link);
    }
}

loadCSSFromQueryParam();

The default is breguet, loading the Breguet dial, but using style=mickey in https://vladris.com/clock/?style=mickey loads the Mickey Mouse face.

Conclusions

CSS is still very cumbersome, but it's impressive how easy it is to achieve quite complex effects with it. Adding a subtle shadow can be done with a box-shadow one-liner. The pendulum animation is a simple rotate transform with 3 keyframes:

@keyframes swing {
    0% {
        transform: rotate(-5deg);
    }
    50% {
        transform: rotate(5deg);
    }
    100% {
        transform: rotate(-5deg);
    }
}

Similarly, having the hands point to the right time is just another rotate. Quite nice!

The full implementation we went over is here.