DEV Community

Cover image for Analog Clock - React
Ming
Ming

Posted on

Analog Clock - React

Create a widget that renders the current time within an analog clock display, where the clock hands move and update like a real clock.

Here's an example of an analog clock, but you can exercise your creativity by choosing different colors and styling it differently. It should however still function as a clock (note: below gif speed a bit slower than normal 1 second. might DEV's render factor🤔)

Image description


Solution

We can separate out the solution into two parts, the update loop and the rendering.

Update loop

We need a timer to refresh the page and display the current time every second. To create the timer, we can use a setInterval and kick off the update loop in a useEffect.

Do remember to clear the timer upon unmounting of the component to prevent "setState on unmounted component" errors and memory leaks.

Store the JavaScript Date object as state as it contains all the data we need.

For better reusability, these logic can be abstracted into a custom useCurrentDate hook.

Rendering

To make the clock more reusable and customizable, we can make the clock size as a property. The clock hands can then be a proportion of the clock's size.

To render the clock hands in the right position, we use a combination of position: absolute and CSS transforms:

  • position: absolute with top and left of half the clock size to make the hands start from the center of the clock. However, the hands will be pointing downwards.
  • Use transform: rotate(180deg) on the clock to make the hands point upwards. This step is optional if we add an 180 degree offset to the rotation angle of the hands.
  • Use transform-origin: center top to make the hands rotate around the center of the clock.
  • Use transform: rotate(Xdeg) on the hands to rotate them into the final angle.

Test Cases

  • See that the second hand updates every second.
  • Observe the clock for a minute to see that the minute hand updates correctly (gradually moved since the observation started).
  • Check that the hour hand's position is different when it's not an exact hour, aka the hour hand's position at 12.00pm and 12.30pm should be different.

Notes
The update frequency of the timer depends on how accurate we want the clock to be. The maximum we can set is 1000ms, however, the clock's accuracy might be off by 1000ms in the case we load the page nearing the last millisecond of the second. However, using too small of an interval can be quite expensive because of too frequent updates. Hence a middleground we've chosen is 100ms. The clock can only ever be off by 100ms, which is not very noticeable by humans.

The current date/time should be polled in each loop, as opposed to recording the time when the clock was first rendered and incrementing based on the interval duration of the timer because the invocations of the loop can be delayed by processes hogging the main thread and the loop may not run at every fixed interval.

Accessibility
For a11y reasons, use a <time> element with datetime attribute set to the current time in 24-hour format so that screen readers can read this component. Otherwise the component will be ignored by screen readers, which is bad. Add the aria-hidden attribute to the internals of <time> since they are for presentation purposes and not useful to screen readers.


AnalogClockWrapper.js

import React from 'react';
import { AnalogClock } from './AnalogClock';

export const AnalogClockWrapper = () => {
  return <AnalogClock />;
};

Enter fullscreen mode Exit fullscreen mode

AnalogClock.js

import React from 'react';
import { useCurrentDate } from './useCurrentDate';
import { ClockImplement } from './ClockImplement';
import './analogclock.css';

export const AnalogClock = () => {
  const date = useCurrentDate();
  //%12 是因为可以把17 --> 变成5  (24hr制)
  const hours = date.getHours() % 12;
  const minutes = date.getMinutes();
  const seconds = date.getSeconds();

  // console.log(date, hours, minutes, seconds);
  return (
    <ClockImplement
      hours={hours}
      minutes={minutes}
      seconds={seconds}
      size={100}
    />
  );
};

Enter fullscreen mode Exit fullscreen mode

ClockImplement.js

import { Hand } from './Hand';

const FULL_ROTATION_DEGREES = 360;
const padTwoDigit = (number) => (number >= 10 ? String(number) : `0${number}`);

export const ClockImplement = ({ hours, minutes, seconds, size }) => {
  const secondsPercentage = seconds / 60;
  const minutesPercentage = (minutes + secondsPercentage) / 60;
  const hoursPercentage = ((hours % 12) + minutesPercentage) / 12;

  const hourAngle = hoursPercentage * FULL_ROTATION_DEGREES;
  const minutesAngle = minutesPercentage * FULL_ROTATION_DEGREES;
  const secondsAngle = secondsPercentage * FULL_ROTATION_DEGREES;

  const dateTimeDisplay = `
    ${padTwoDigit(hours)}:${padTwoDigit(minutes)}:${padTwoDigit(seconds)}
  `;

  return (
    <time
      className='clock'
      dateTime={dateTimeDisplay}
      style={{
        '--size': `${size}px`,
      }}
    >
      <Hand height={0.5} angle={hourAngle} width={3} />
      <Hand height={0.9} angle={minutesAngle} width={2} />
      <Hand height={0.8} angle={secondsAngle} />
    </time>
  );
};

Enter fullscreen mode Exit fullscreen mode

Hand.js

export const Hand = ({ height = 1, width = 1, angle }) => {
  return (
    <div
      aria-hidden={true}
      className='clock-hand'
      style={{
        transform: `rotate(${angle}deg) scaleY(${height}) scaleX(${width})`,
      }}
    />
  );
};

Enter fullscreen mode Exit fullscreen mode

useCurrentDate.js

import { useEffect, useState } from 'react';

export const useCurrentDate = () => {
  const [date, setDate] = useState(new Date());

  // kick off the timer
  useEffect(() => {
    const timer = window.setInterval(() => {
      setDate(new Date());
    }, 1000);

    // Clear the timer upon unmount
    return () => {
      window.clearInterval(timer);
    };
  }, []);

  return date;
};

Enter fullscreen mode Exit fullscreen mode

analogclock.css


.clock {
  display: block;
  position: relative;
  width: var(--size);
  height: var(--size);
  border-radius: 100%;
  border: 2px solid #ccc;
  transform: rotate(180deg);
}

.clock-hand {
  background-color: black;
  position: absolute;
  width: 1px;
  height: calc(var(--size) / 2);
  left: calc(var(--size) / 2);
  top: calc(var(--size) / 2);
  transform-origin: top center;
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)

Super Useful CSS Resources

A collection of 70 hand-picked, web-based tools which are actually useful.
Each will generate pure CSS without the need for JS or any external libraries.