Hajime, the duck guy

Friday, May 31, 2024, by Hajime Yamasaki Vukelic

The clock widget

Someone pointed me to a tutorial for creating a simple analog clock widget. I've never done an analog clock before, so I got curious.

I started watching the tutorial, but then — as usual — I decided to first give it a shot without following the tutorial, and then compare notes afterwards. In the process, I caught myself doing a few things that might be interesting to some of you, so I'm sharing my notes here.

This is what the finished result looks like:

Screenshot of the clock widget
Final version of the clock widget

You can find the complete code in the Responsive analog clock widget pen.

The HTML

The HTML for the clock widget looks like this:

<x-clock style="--hour: 12; --minute: 0; --second: 0">
    <div class="mark"></div>
    <div class="mark"></div>
    <div class="mark"></div>
    <div class="mark"></div>
    <div class="mark"></div>
    <div class="mark"></div>
    <div class="hour-hand"></div>
    <div class="minute-hand"></div>
    <div class="second-hand"></div>
    <div class="text-alt" aria-live="polite"></div>
</x-clock>

Whenever I want to add behavior to a set of elements using JavaScript, I like to wrap them in a custom element. This makes it easier for me to add the same behavior to multiple regions of the page if I need to do so. It also gives me a convenient namespace for styling.

Before you ask, the reason I add an x- prefix to the element name is that custom elements need to have at least two words separated by a dash, so rather than inventing some weird prefix, I just use x-, imitating the custom HTTP headers.

You will notice that we have three custom CSS properties that look like time. The intention is to use these properties to control the clock hand position in CSS. I can also render the clock in a specific time on the server and remove the JavaScript entirely to get a static analog clock on the page.

The <div class="mark"> elements represent the 12 marks around the perimeter of the clock face. I only need 6 of those because each can have two pseudo-elements — ::after and ::before.

The next three elements represent the hour, minute and second hands, respectively.

The <div class="text-alt"> is used to store the text alternative for the whole clock. It's similar to the alt attribute on the image, and this is the only part of the whole contraption the screen reader will care about. The <div> is marked as a live region, so that anything we put in it will be spoken out as soon as possible.

The JavaScript

As discussed before, the JavaScript mainly just controls the three custom CSS properties, and not much else, so it's relatively straightforward.

I start by registering the custom element.

customElements.define('x-clock', class extends HTMLElement {
    connectedCallback() {
        // All code will go here
    }
})

I will place all the code within the connectedCallback() method. Through experimenting, I've arrived at the conclusion that this lets me get away with the least amount of code. I add other methods only as required by the custom elements API. From this point on, assume that any JavaScript is part of the connectedCallback().

The clock automatically updates on its own to keep the time. For this, I will use a recursive animation frame callback.

requestAnimationFrame(function nextFrame() {
    // Update the clock here
    requestAnimationFrame(nextFrame)
})

I kick the loop off by calling requestAnimationFrame(). In the callback, I update the widget, and then recurse into the next frame. The nextFrame() callback is named, so that I can call it from within itself. This callback does two things:

  1. set the --hour, --minute, and --second custom properties to appropriate values
  2. update the text alternative every minute

Let me first walk you through the part of the code that updates the custom properties:

var style = this.style

requestAnimationFrame(function nextFrame() {
    var now = new Date()

    // Uncomment the code below to make second hand move smoothly
    var second = now.getSeconds()// + now.getMilliseconds() / 1000;
    var minute = now.getMinutes() + second / 60;
    var hour = now.getHours() + minute / 60;

    style.setProperty('--hour', hour)
    style.setProperty('--minute', minute)
    style.setProperty('--second', second)

    // ...
})

I store the style property of the custom element so I can refer to it in the callback. The code is pretty straightforward, so I will only explain why it's done that way. The minute and hour hand on real clocks aren't strictly indexed to their minute/hour marks. Instead, they move gradually as the second hand moves — for example, at 6:30, the hour hand sits between the 6 and the 7 mark. This is why the minute and hour values include the fractional parts. The second hand can either index with the minute mark or move smoothly. I've provided the smoothed-out version in the comment.

And now for the code that sets the text alternative.

var $textAlt = this.querySelector('.text-alt')

requestAnimationFrame(function nextFrame() {
    // ...

    var altText = `Current time is ${now.toLocaleTimeString('en', {
        hour: 'numeric',
        minute: 'numeric',
    })}`

    if (altText != $textAlt.textContent)
        $textAlt.textContent = altText

    // ...
})

I store a reference to the .text-alt element so that I don't have to query for it on evey frame. Because the text alternative element is a live region, I cannot just set the value every time. I need to set it only if it's different from the current one. Otherwise the screen reader will keep reading same text every frame most of the time, which would be annoying to say the least.

The $ prefix on the variable doesn't have any special meaning. It's just some jQuery nostalgia. I'm joking. I like to prefix the variable with a $ when it contains a reference to a DOM node.

One thing that may have caught your eye is the comparison with $textAlt.textContent. You may think it's rather expensive to do every frame. I've also tested a version that caches the current value in a variable and there was no measurable difference, so I opted to go with the version without the extra variable.

The CSS

CSS is where most of the action is happening. Because there's a lot of it (well, a lot for a blog post), I'm just going to highlight the interesting parts.

Let me first show you the code for the clock face itself:

x-clock {
    /* ... */
    width: min(30em, 80vmin);
    aspect-ratio: 1;
    /* ... */
}

Note the expression min(30em, 80vmin). This means that the clock size will be whichever happens to be less between 30em and 80vmin, where 80vmin is 80% of the smaller viewport dimension — it can be either the width or the height. As you change the viewport size, the clock will shrink and expand up to the maximum size of 30em. I use aspect-ratio: 1 so that I don't need to repeat these values for the height — the width property serves as the single source of truth, in other words.

I use % for all lengths in elements inside the <x-clock> tag. This means they all scale proportionately with the face. The only lengths that use em are shadow-related ones, because box-shadow does not support %. Since the widget contains no visible text — it's purely graphical — I'm basing the size of the elements on the dimensions of the outermost container, instead of the text. This is more of an exception in how I design things as usually it's the text that dictates the other dimensions.

Using % units for border-radius was a bit tricky, but eventually I learned of the form that includes a slash — e.g., border-radius: 30% / 3% — and that solved the problem. (I keep getting reminded that one can never read the docs enough times.) Usually, using a single value for the border-radius property works as we'd expect. However, when using a single percentage length, the corner is an ellipse in all cases except when the box is a square. This is because the percentage applies separately to the lengths of the two edges that form the corner. Using the X% / Y% format, I specify the percentages separately for each edge. You will find more details in the border-radius specs.

The rotation of the hands is specified using calc() formulas that references the custom properties I mentioned earlier. For example:

x-clock .hour-hand {
    /* ... */
    transform: rotate(calc(var(--hour) * 30deg));
}

In the case of the hours, the 30deg value represents the angle of a single hour.

Conclusion

Here are the main take-aways from this exercise:

  • You can pass values to CSS from from HTML using custom CSS properties in the style attribute. (I will also argue that this is the only legitimate use of the style attribute.)
  • You can move calculations to CSS and thus reduce the amount of JavaScript.
  • When you rig the whole widget so that most of the work is done by HTML and CSS, you don't have to write as much JavaScript to make it do things.
  • When presenting a purely graphical element such as this analog clock, using percentage lengths internally will make the graphic scale proportionately to the outermost container.
Posted in Work in progress
Back to top