Clocks Made of Clocks: A Masterpiece of Creative Coding and React 🕒✨
Hello everyone! Welcome back to the blog. Today, I am incredibly excited to share a digital masterpiece that is a perfect blend of coding logic and visual art. If you are looking for a unique project to showcase in your portfolio or simply want to understand the power of React and CSS animations, the "Clocks Made of Clocks" project is exactly what you need.
In this post, I will break down how dozens of tiny analog clocks work together to form a large, functional digital clock. I have provided the source code in separate files below, each with a convenient Copy Button for you. 🚀
The Concept: Kinetic Typography with Code ðŸ§
Most digital clocks rely on standard fonts or static images. However, this project uses Kinetic Typography. Imagine using the hands of an analog clock as individual "pixels." By rotating these hands to specific angles, we can create the shapes of numbers.
Every digit in this clock is made up of a grid of 24 small clocks (4 columns and 6 rows). When the time changes, the hands of these small clocks rotate in sync to form the new number. It is a mesmerizing visual experience that keeps users engaged on your website.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Clock made of clocks</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="./script.js"></script>
</body>
</html>
body { padding: 0; margin: 0; }
*, *::before, *::after { box-sizing: border-box; }
.app {
--clock-size: 3vw;
--gap: calc(var(--clock-size) * 0.05);
--clock-segment-w: calc(var(--clock-size) * 4 + var(--gap) * 5);
--clock-segment-h: calc(var(--clock-size) * 6 + var(--gap) * 5);
font-family: sans-serif;
text-align: center;
display: flex;
gap: var(--gap);
align-items: center;
justify-content: center;
height: 100vh;
padding-left: calc(var(--clock-size) + var(--gap) * 2);
}
.app > div { display: flex; flex-wrap: wrap; gap: var(--gap); width: var(--clock-segment-w); height: var(--clock-segment-h); }
.clock {
--w: 47%; --h: 3px; --dur: 0.4s; position: relative; width: var(--clock-size); height: var(--clock-size); border-radius: 50%; border: 2px solid white;
background: linear-gradient(225deg, #d0d0d0 10%, white); box-shadow: -2px 2px 6px #d0d0d0, 2px -2px 6px #ffffff;
}
.clock::before, .clock::after {
position: absolute; content: ''; top: calc(50% - var(--h) * 0.5); left: 50%; transform-origin: 0% 50%; width: var(--w); height: var(--h); background: black; border-radius: 9999px; transition: calc(var(--dur) * 1s) ease-in-out; transform: rotate(calc(var(--angle) * 1deg));
}
.app > div:nth-of-type(even) { margin-right: var(--clock-size); }
.clock::before { --angle: var(--hour-angle); }
.clock::after { --angle: var(--minute-angle); }
@media (max-width: 700px) { .clock { --h: 2px; border: 1px solid white; } }
@media (max-width: 500px) { .clock { --h: 1px; --w: 50%; } }
import React, { useState, useEffect, useRef } from "https://esm.sh/react@19";
import { createRoot } from "https://esm.sh/react-dom@19/client";
const H = { h: 0, m: 180 }, V = { h: 270, m: 90 }, TL = { h: 180, m: 270 }, TR = { h: 0, m: 270 }, BL = { h: 180, m: 90 }, BR = { h: 0, m: 90 }, E = { h: 135, m: 135 };
const digits = [
[BR, H, H, BL, V, BR, BL, V, V, V, V, V, V, V, V, V, V, TR, TL, V, TR, H, H, TL],
[BR, H, BL, E, TR, BL, V, E, E, V, V, E, E, V, V, E, BR, TL, TR, BL, TR, H, H, TL],
[BR, H, H, BL, TR, H, BL, V, BR, H, TL, V, V, BR, H, TL, V, TR, H, BL, TR, H, H, TL],
[BR, H, H, BL, TR, H, BL, V, E, BR, TL, V, E, TR, BL, V, BR, H, TL, V, TR, H, H, TL],
[BR, BL, BR, BL, V, V, V, V, V, TR, TL, V, TR, H, BL, V, E, E, V, V, E, E, TR, TL],
[BR, H, H, BL, V, BR, H, TL, V, TR, H, BL, TR, H, BL, V, BR, H, TL, V, TR, H, H, TL],
[BR, H, H, BL, V, BR, H, TL, V, TR, H, BL, V, BR, BL, V, V, TR, TL, V, TR, H, H, TL],
[BR, H, H, BL, TR, H, BL, V, E, E, V, V, E, E, V, V, E, E, V, V, E, E, TR, TL],
[BR, H, H, BL, V, BR, BL, V, V, TR, TL, V, V, BR, BL, V, V, TR, TL, V, TR, H, H, TL],
[BR, H, H, BL, V, BR, BL, V, V, TR, TL, V, TR, H, BL, V, BR, H, TL, V, TR, H, H, TL]];
const normalizeAngle = (next, prev) => {
const delta = ((next - prev) % 360 + 360) % 360;
return prev + delta;
};
const getTimeDigits = () => {
const now = new Date();
return [now.getHours(), now.getMinutes(), now.getSeconds()].flatMap(val => String(val).padStart(2, "0").split("").map(Number));
};
const randomAngle = () => Math.floor(Math.random() * 360);
const Clock = ({ h, m, initial }) => {
const prev = useRef({ h: 0, m: 0 });
const hourAngle = normalizeAngle(h, prev.current.h);
const minuteAngle = normalizeAngle(m, prev.current.m);
prev.current = { h: hourAngle, m: minuteAngle };
return React.createElement("div", { className: "clock", style: { "--hour-angle": initial ? randomAngle() : hourAngle, "--minute-angle": initial ? randomAngle() : minuteAngle, "--dur": initial ? 1 : 0.4 } });
};
const App = () => {
const [time, setTime] = useState(Array(6).fill(0));
const [initial, setInitial] = useState(true);
useEffect(() => {
let updateTimerId;
const updateTime = () => {
setTime(getTimeDigits());
const now = Date.now();
const delay = 1000 - now % 1000;
updateTimerId = setTimeout(updateTime, delay);
};
const initialTimerId = setTimeout(() => { setInitial(false); updateTime(); }, 600);
return () => { clearTimeout(updateTimerId); clearTimeout(initialTimerId); };
}, []);
return React.createElement("div", { className: "app" }, time.map((t, i) => React.createElement("div", { key: i }, digits[t].map(({ h, m }, j) => React.createElement(Clock, { key: j, h: h, m: m, initial: initial })))) );
};
createRoot(document.getElementById('root')).render(React.createElement(App, null));
Conclusion
Creating a project like this is a great way to improve your skills in React and CSS. It challenges your mathematical thinking while allowing you to create something visually stunning. 🌈
If you find this code helpful, feel free to leave a comment below. Don't forget to subscribe for more tutorials! Happy Coding! 💻🔥

