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" />