Hand of time

Among the JavaScript libraries to draw on the web Raphaël offers an exciting, if surpassed, API. On the website you find a sundry of demos promoting the library and one in particular impressed me: hand, a display plotting hours in a twelve-hour clock with colorful circles and one hand with the qualities of a chameleon gifted with quick reflexes. Click one of the circles and the color of the nimble marker changes to match, smoothly over time. In parallel, the hand bounces from its previous location to the one you just pointed. It is a cute interaction which I tried to re-build on my own.

Starting with a series of circles, I resolved to use <circle> elements changing the coordinate system and pushing the shapes around in a clockwise pattern.

<circle transform="rotate(0) translate(0 -18)" />
<circle transform="rotate(30) translate(0 -18)" />
<!-- ...60, 90, ...330 -->

The idea of perceived lightness pushed me to ditch hsl and explore the oklch syntax.

<circle style="color: oklch(0.8 0.17 200)" />
<circle style="color: oklch(0.8 0.17 230)" />

By increasing the third parameter, for the hue, you obtain the colors in the entire 360 degrees spectrum — you’ll just have to agree that red, green and blue don’t coincide with the HSL staples of 0, 120 and 240; they are slightly offset.

For the hand I attempted a <path> with a bezier curve reaching up before tracing the contours of the round shapes.

<path
  d="M 0 0 C 5 -7.75 -5 -7.75 0 -15.5 A 2.5 2.5 0 0 0 0 -20.5 2.5 2.5 0 0 0 0 -15.5"
/>

From this point the “easy” part is to modify two values for the central figure. Just change the angle to rotate the <path> while changing the color as well — stroke will pick up the hint through currentColor.

<path
  style="color: oklch(0.8 0.17 230); rotate(30deg)"
  fill="none"
  stroke="currentColor"
/>

To trigger the update I considered listening to a click event on the single circles, but these elements are not really meant to be interactive. Promptly, you can add anchor links in SVG, so you can borrow the control node for the interaction.

<a href="/">
  <!-- ...circles -->
</a>

One anchor link around the entire set with a prompt call to prevent the default re-routing whenever pressed.

link.addEventListener("click", (event) => {
  event.preventDefault();
});

You need to know which circle was pressed, so a data attribute can carry the information, the index of the chosen shape.

<a href="/">
  <circle data-index="0" />
  <circle data-index="1" />
</a>

Extract the value from the target of the click event and update the values with the ones you took care to cache in a custom array.

const index = parseInt(event.target.getAttribute("data-index"));
const { color, angle } = circles[index];
path.style.transform = `rotate(${angle}deg)`;
path.style.color = color;

For the smooth change there’s no need to interpolate the angle, and neither the color. Just ask CSS to pick up the slack with the transition property.

path {
  transition-property: transform, color;
  transition-duration: 1.5s;
}

And linear for the timing function offers a custom, bouncy effect — for the hue as well!

path {
  transition-timing-function: linear(0, 0.004, 0.016, 0.035, 0.062, 0.098, 0.141 11.4%, 0.25, 0.39, 0.562, 0.764, 1 30.3%, 0.847 34.8%, 0.787, 0.737, 0.699, 0.672, 0.655, 0.65, 0.656, 0.672, 0.699, 0.738, 0.787, 0.847 61.7%, 1 66.2%, 0.946, 0.908, 0.885 74.2%, 0.879, 0.878, 0.879, 0.885 79.5%, 0.908, 0.946, 1 87.4%, 0.981, 0.968, 0.96, 0.957, 0.96, 0.968, 0.981, 1);
}

Sweet, but you can go further. Links can be focused by keyboard, tabbing through the interface. So just consider a keydown event waiting for more instructions.

link.addEventListener("keydown", (event) => {
  const { key } = event;
  if (isNaN(parseInt(key, 10))) return;
  event.preventDefault();
});

One-off keypresses work for small numbers, but that won’t do the trick for all options. To ponder a two-digit number I went with a timeout and a short, but noticeable, delay.

let timeout;
const delay = 200;
let input = "";

Process the input and if it turns out to be an hour in the well-reasoned range, say 1 to 12, update the display just like you did for the click event.

input = `${input + key}`.slice(-2);
clearTimeout(timeout);
timeout = setTimeout(() => {
  const number = parseInt(input, 10);
  if (number > 0 && number <= n) {
    const index = number % n;
    // update path with circles[i]
  }
  input = "";
  clearTimeout(timeout);
}, delay);

It works!

To wrap the result with a beautiful bow you can change the opacity, and even the opacity of the fill only, when you hover on the circles.

a circle {
  fill-opacity: 0.5;
}

a circle:hover {
  transition: fill-opacity 0.4s;
  fill-opacity: 1;
}

And if you forgo the default outline on the one link be sure not to forget a rule for the focus state.

a {
  outline: none;
}

svg:has(:focus-visible) {
  outline: 1px solid currentColor;
  outline-offset: 2px;
}

In summation, this is how you can create an interactive, gorgeous display with SVG syntax, highjacking built-in control nodes and delegating much of the hard work to the support of modern syntaxes and complex formulas. This is also one way you are able to collect more than a single number in quick succession, delaying what would otherwise happen without restrain. You can. With only one question left: should you?