Synced Clock Animation

January 04 2026

·

4 min read

A Creative Experiment in Time Visualization

What if you could tell time not with one clock, but with 144 mini-clocks, each representing a different city around the world? That's exactly what I built. A digital clock display where every digit is formed by an array of 24 animated clock faces, and each clock face shows the time in a different global city.

The Concept

The core idea is beautifully recursive: use clocks to display clock digits. Each of the six digits in a time display (HH:MM:SS) is rendered as a 4×6 grid of 24 individual clock faces. These mini-clocks don't just sit there—they actively rotate their hands to form the shape of each digit, creating a mesmerizing animation whenever the time changes.

The Architecture

1. Digit Representation with Clock Hands

The foundation of this project is mapping each digit (0-9) to a pattern of clock hand positions:

type Combination = [number, number]; // [angle1, angle2]
 
const combinations = {
  'bottom-right-corner': [90, 0] as Combination,
  'bottom-left-corner': [180, 90] as Combination,
  'top-left-corner': [270, 180] as Combination,
  'top-right-corner': [270, 0] as Combination,
  straight: [90, 270] as Combination,
  line: [180, 0] as Combination,
  unused: [-220, -220] as Combination,
};
 
// Example: The digit "0" forms an outline
const digit_0: Combination[] = [
  combinations['bottom-right-corner'],
  combinations['line'],
  combinations['line'],
  combinations['bottom-left-corner'],
 
  combinations['straight'],
  combinations['bottom-right-corner'],
  combinations['bottom-left-corner'],
  combinations['straight'],
 
  combinations['straight'],
  combinations['straight'],
  combinations['straight'],
  combinations['straight'],
 
  combinations['straight'],
  combinations['straight'],
  combinations['straight'],
  combinations['straight'],
 
  combinations['straight'],
  combinations['top-right-corner'],
  combinations['top-left-corner'],
  combinations['straight'],
 
  combinations['top-right-corner'],
  combinations['line'],
  combinations['line'],
  combinations['top-left-corner'],
];
 
const numbers: Record<number, Combination[]> = {
  0: digit_0,
  1: digit_1,
  // ... 2-9
};

Each position uses predefined "combinations" of two clock hands:

  • Corner pieces: Two hands at 90° apart forming corners
  • Lines: Two hands at 180° apart forming straight edges
  • Unused: Hidden hands for negative space

2. The World Clock Layer

Here's where it gets interesting—each of those 144 mini-clocks represents a real city:

type City = {
  label: string;
  timeZone: string;
};
 
const cities: City[] = [
  { label: 'New York', timeZone: 'America/New_York' },
  { label: 'Tokyo', timeZone: 'Asia/Tokyo' },
  { label: 'London', timeZone: 'Europe/London' },
  // ... 141 more cities
];
 
// Ensure exactly 144 cities (pad if needed)
const cities144: City[] = (() => {
  const list = cities.filter((c) => c.timeZone && c.label);
  const unique = list.slice(0, 144);
  while (unique.length < 144) {
    unique.push(list[unique.length % list.length]);
  }
  return unique.slice(0, 144);
})();
 
// Distribute cities across all 144 clock positions
// 6 digits × 24 clocks per digit = 144 total clocks
// cities[0...23]    - hour tens digit
// cities[24...47]   - hour ones digit
// cities[48...71]   - minute tens digit
// cities[72...95]   - minute ones digit
// cities[96...119]  - second tens digit
// cities[120...143] - second ones digit

When you hover over any mini-clock, you see which city it represents and what time it is there.

3. Smooth Animations

The magic happens in the transitions. When a digit changes (say, from 23:59 to 00:00), all affected clocks smoothly rotate to their new positions:

const shortestDelta = (from: number, to: number): number => {
  const mod = (value: number, modulus: number) =>
    ((value % modulus) + modulus) % modulus;
 
  const fromNorm = mod(from, 360);
  return mod(to - fromNorm + 540, 360) - 180;
};
 
const animateToTarget = (currentAngle: number, targetAngle: number): number => {
  // Calculate shortest rotation path
  const delta = shortestDelta(currentAngle, targetAngle);
 
  // Return new angle (React will handle smooth transition)
  return currentAngle + delta;
};
 
// Usage in component
useEffect(() => {
  setAngleA((prev) => animateToTarget(prev, targetA));
  setAngleB((prev) => animateToTarget(prev, targetB));
}, [targetA, targetB]);

This ensures hands always take the shortest path, never spinning unnecessarily.

4. Real-Time Updates

The entire display updates every second:

const [hour, setHour] = useState<number>(0);
const [minute, setMinute] = useState<number>(0);
const [second, setSecond] = useState<number>(0);
const [timeZone, setTimeZone] = useState<string | undefined>(undefined);
 
useEffect(() => {
  const updateTime = () => {
    const now = new Date();
 
    if (timeZone) {
      // Get time in selected timezone
      const timeString = now.toLocaleTimeString('en-GB', {
        timeZone,
      });
      const [h, m, s] = timeString.split(':').map(Number);
      setHour(h);
      setMinute(m);
      setSecond(s);
    } else {
      // Use local time
      setHour(now.getHours());
      setMinute(now.getMinutes());
      setSecond(now.getSeconds());
    }
  };
 
  updateTime();
  const interval = setInterval(updateTime, 1000);
 
  return () => clearInterval(interval);
}, [timeZone]);
 
// Calculate digit values
const hourTens = Math.floor(hour / 10);
const hourOnes = hour % 10;
const minuteTens = Math.floor(minute / 10);
const minuteOnes = minute % 10;
const secondTens = Math.floor(second / 10);
const secondOnes = second % 10;

5. Interactive Time Travel

The interactive element lets you click any clock to switch the entire display to that city's timezone:

const handleCitySelect = (selectedTimeZone: string) => {
  setTimeZone(selectedTimeZone);
  // All 144 clocks now show time in selected timezone
  // Digit display updates to show that time
};
 
const handleResetToLocal = () => {
  setTimeZone(undefined);
  // Return to showing local time
};
 
// In ClockNode component
<div
  onClick={() => city && onCitySelect?.(city.timeZone)}
  className="cursor-pointer"
>
  {/* Clock hands */}
</div>

Technical Highlights

  • React hooks for state management and side effects
  • CSS transforms for smooth hand rotations with hardware acceleration
  • Intl.DateTimeFormat API for timezone-aware time formatting
  • Grid layouts for precise positioning of 144 individual elements
  • Modular design with reusable clock components

The Result

What you get is a hypnotic visualization where:

  • Time flows through 144 synchronized mini-clocks
  • Each clock represents a real city somewhere in the world
  • Digit transitions create beautiful choreographed animations
  • You can instantly jump to any global timezone with a click

It's a testament to how creative constraints—representing digits purely through clock hand positions—can lead to unexpectedly engaging results.


Try it yourself: Click on any clock to see what time it is in that city. Watch how all 144 clocks work together to tell you the time, each one simultaneously serving its own purpose and contributing to the larger display.