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:
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:
- set the
--hour,--minute, and--secondcustom properties to appropriate values - 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
styleattribute. (I will also argue that this is the only legitimate use of thestyleattribute.) - 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.