Build a Clock Face with SVG in React Native

In this article, we are going to look at how to draw a nice-looking analog clock face by using react-native, react-native-svg, and styled-components. The clock is going to tell time, tick, and have support for dark and light themes.

This is based on the work I’ve done on Nyxo, where we show a clock face as the base for displaying your sleep data. I’ve been getting some questions on how to create something similar, which is why I wrote this guide to help you create them yourselves. If you just want to get your hands on the code, it’s here.

Getting started

Let's start by initializing a new project. You could very well do this with Expo, but I prefer to use the ejected version of React Native, so we are going to use the react-native-cli and the TypeScript example project to get started:

npx react-native init helloClock --template react-native-template-typescript

After that, let's install the only external library we need: react-native-svg.

npm install react-native-svg

Then navigate to the ios folder and install the required pods:

cd ios && pod install && cd -

You can now run the project:

react-native run-ios

Folder structure

I'm going to structure the project in the following way:

HelloClock
├── index.js
├── App.tsx
├── components
│   ├── Hand.tsx
│   ├── ClockMarkings.tsx
│   └── Clock.tsx
├── helpers
│   ├── geometry.ts
│   ├── time.ts
│   └── useInterval.ts

Polar and Cartesian Coordinates

Now to the bread and butter of this article: how to convert time to coordinates on SVG.

We can think of clock times in degrees, i.e., 12 am and 12 pm being the same as 0°, and 6 pm and 6 am being 180°. We could, of course, use radians as well, but degrees feel more familiar to most people. A coordinate system that uses an angle and a reference point to determine a point on a plane is called the Polar coordinate system.

Converting time to Polar coordinate systems is relatively simple. For example, to determine the angle of the minute hand on a clock in degrees when the number of minutes is 30: if one full revolution is 60 minutes and one complete revolution is 360°, then dividing 30 minutes by 60 and multiplying that by 360 gives the same number of minutes in degrees, which is 180°. Let's implement that in the geometry.ts file:

export function polarToCartesian(
  centerX: number,
  centerY: number,
  radius: number,
  angleInDegrees: number,
) {
  const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;

  return {
    x: centerX + radius * Math.cos(angleInRadians),
    y: centerY + radius * Math.sin(angleInRadians),
  };
}

Building the Clock Component

Let’s start the UI work by cleaning up the App.tsx file so that it only includes a StatusBar, SafeAreaView, and the <Clock> component:

// App.tsx
import React from "react";
import Clock from "./components/Clock";
import styled from "styled-components/native";

const App = () => {
  return (
    <>
      <StatusBar barStyle="light-content" />
      <SafeAreaView>
        <ScrollView
          centerContent={true}
          contentInsetAdjustmentBehavior="automatic">
          <Clock />
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

const ScrollView = styled.ScrollView`
  flex: 1;
  background-color: black;
`;

const SafeAreaView = styled.SafeAreaView`
  background-color: black;
  flex: 1;
`;

const StatusBar = styled.StatusBar.attrs(() => ({
  barStyle: "light-content",
}))``;

export default App;

Next, we’ll work on the Clock, ClockMarkings, and Hand components. In Clock.tsx, import Dimensions from react-native and the Svg component from react-native-svg. Make the Clock component return a square SVG with the side of the square being the same as the width of the mobile phone's screen using the Dimensions helper:

// Clock.tsx
import React from "react";
import Svg from "react-native-svg";
import { Dimensions } from "react-native";
const { width } = Dimensions.get("window");

const Clock = () => {
  return <Svg height={width} width={width}></Svg>;
};

export default Clock;

When saved, nothing will appear on the screen since SVG itself has no visible parts. Let’s continue by adding the ClockMarkings to communicate the minutes and hours. First, define how many ticks we want by writing these values above the Clock component:

// Clock.tsx
const diameter = width - 40;
const center = width / 2;
const radius = diameter / 2;
const hourStickCount = 12;
const minuteStickCount = 12 * 6;

const Clock = () => {
  return <Svg height={width} width={width}></Svg>;
};

export default Clock;

Now let’s create the ClockMarkings component in ClockMarkings.tsx and render the hour and minute ticks:

// ClockMarkings.tsx
import React from "react";
import { G, Line, Text } from "react-native-svg";
import { polarToCartesian } from "../helpers/geometry";

type Props = {
  radius: number;
  center: number;
  minutes: number;
  hours: number;
};

const ClockMarkings = (props: Props) => {
  const { radius, center, minutes, hours } = props;
  const minutesArray = new Array(minutes).fill(1);
  const hoursArray = new Array(hours).fill(1);

  const minuteSticks = minutesArray.map((_, index) => {
    const start = polarToCartesian(center, center, radius, index * 5);
    const end = polarToCartesian(center, center, radius, index * 5);
    return (
      <Line
        stroke="white"
        strokeWidth={2}
        strokeLinecap="round"
        key={index}
        x1={start.x}
        x2={end.x}
        y1={start.y}
        y2={end.y}
      />
    );
  });

  const hourSticks = hoursArray.map((_, index) => {
    const start = polarToCartesian(center, center, radius - 10, index * 30);
    const end = polarToCartesian(center, center, radius, index * 30);
    const time = polarToCartesian(center, center, radius - 35, index * 30);

    return (
      <G key={index}>
        <Line
          stroke="white"
          strokeWidth={3}
          strokeLinecap="round"
          x1={start.x}
          x2={end.x}
          y1={start.y}
          y2={end.y}
        />
        <Text
          textAnchor="middle"
          fontSize="17"
          fontWeight="bold"
          fill="white"
          alignmentBaseline="central"
          x={time.x}
          y={time.y}>
          {index === 0 ? 12 : index}
        </Text>
      </G>
    );
  });

  return (
    <G>
      {minuteSticks}
      {hourSticks}
    </G>
  );
};

export default ClockMarkings;

Now if we add the ClockMarkings component to the Clock component and pass the variables for the clock face, we get a basic clock face.

Adding alt texts

In the article's images, provide meaningful alt attributes for better accessibility:

<Image src={ClockMarkings} alt="Illustration of clock markings showing hours and minutes" />
<Image src={Clock} alt="Completed clock face with hands showing time" />