Build an analog clock with HTML and CSS

Three divs, one CSS rule that moves the pivot, and a formula for each hand. The page below is the smallest version, and the sections after it cover smooth hands, time zones and the usual mistakes.

An analog clock needs a round face, three hands that turn around its center, and a few lines of JavaScript that read the time and set each hand's angle. CSS does the turning with transform: rotate(). JavaScript only works out the numbers.

Here is the smallest working version. It is one HTML file with no library and no image.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Analog clock</title>
<style>
  body {
    margin: 0; min-height: 100vh; display: grid; place-content: center; justify-items: center; gap: 14px;
    font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330;
  }
  .face {
    position: relative; box-sizing: border-box;
    width: min(70vw, 260px); aspect-ratio: 1;
    border: 6px solid #1d2330; border-radius: 50%; background: #fff;
  }
  .hand {
    position: absolute; left: 50%; bottom: 50%;   /* the hand's foot sits on the center */
    transform-origin: 50% 100%;                   /* ...and it turns around its foot */
    border-radius: 4px; background: #1d2330;
  }
  .hour   { width: 6px; height: 26%; margin-left: -3px; }
  .minute { width: 4px; height: 38%; margin-left: -2px; }
  .second { width: 2px; height: 42%; margin-left: -1px; background: #e0482b; }
  .pin { position: absolute; left: 50%; top: 50%; width: 12px; height: 12px; margin: -6px; border-radius: 50%; background: #e0482b; }
</style>
</head>
<body>
<div class="face" role="img" aria-label="Analog clock">
  <div class="hand hour" id="hour"></div>
  <div class="hand minute" id="minute"></div>
  <div class="hand second" id="second"></div>
  <div class="pin"></div>
</div>
<div id="readout">--:--:--</div>

<script>
  const hour = document.getElementById('hour');
  const minute = document.getElementById('minute');
  const second = document.getElementById('second');
  const readout = document.getElementById('readout');

  function tick() {
    const d = new Date();
    const s = d.getSeconds();                 // 0-59
    const m = d.getMinutes();                 // 0-59
    const h = d.getHours() % 12;              // 0-11, a 12-hour dial

    second.style.transform = 'rotate(' + (s * 6) + 'deg)';              // 360 / 60
    minute.style.transform = 'rotate(' + (m * 6 + s * 0.1) + 'deg)';    // creeps between minutes
    hour.style.transform   = 'rotate(' + (h * 30 + m * 0.5) + 'deg)';   // creeps between hours

    readout.textContent = d.toLocaleTimeString();
  }

  tick();                    // draw now, so the dial is not at 12:00:00 for a second
  setInterval(tick, 1000);
</script>
</body>
</html>
Three divs for the hands and one function that sets their angles every second. Edit the code and the clock reruns.

How it works, step by step

Every analog clock built this way follows the same five steps.

  1. Draw the face. A div with border-radius: 50% and aspect-ratio: 1 is a circle that stays round at any width.
  2. Stand each hand on the center. Give the hand position: absolute, left: 50% and bottom: 50%. Its foot now sits on the middle of the dial.
  3. Move the pivot to the foot. Set transform-origin: 50% 100% so the hand turns around its foot.
  4. Turn the time into angles. getSeconds(), getMinutes() and getHours() return the numbers, and a short formula turns each one into degrees.
  5. Write the angle, then repeat. Assign a string such as rotate(90deg) to style.transform. Call the function once straight away, then again on an interval.

The hands are drawn pointing straight up, so an angle of 0deg reads 12 o'clock. In a left-to-right page a positive angle turns clockwise, which is the way a clock runs.

Step 3 is easy to skip. An element turns around its own center by default, because the initial value of transform-origin is 50% 50%.

For a hand that is the wrong point. The middle of the hand stays put, and its foot swings away from the dial's center.

Left: the default origin turns the hand around its middle. Right: an origin at the foot turns it around the center of the dial.
Left: the default origin turns the hand around its middle. Right: an origin at the foot turns it around the center of the dial.

The transform-origin guide covers the keywords and percentages in more detail.

The angle formulas

A full turn is 360 degrees. The second hand makes one turn in 60 seconds and the minute hand one turn in 60 minutes, so each step is 6 degrees. A 12-hour dial gives the hour hand 30 degrees per hour.

The three hands at 3:20:40, with the formula and the resulting angle for each.
The three hands at 3:20:40, with the formula and the resulting angle for each.
Hand Angle in degrees Why
Second seconds * 6 360 / 60
Minute minutes * 6 + seconds * 0.1 6 per minute, plus 0.1 per second
Hour (hours % 12) * 30 + minutes * 0.5 30 per hour, plus 0.5 per minute

The extra terms keep the minute and hour hands moving between marks. Without them, the hour hand at 3:30 would point straight at the 3 instead of midway to the 4.

getHours() returns a number from 0 to 23 in local time, so % 12 folds it onto a 12-hour dial. All three getters read the device's own clock.

Why the second hand spins backward at :00

Adding transition: transform .5s looks like an easy way to get a smooth hand. It works until the seconds wrap from 59 back to 0.

The angle then goes from 354deg to 0deg. The browser moves between those two values in a straight line, so the hand whips backward through the whole dial.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Second hand wrap</title>
<style>
  body {
    margin: 0; padding: 14px 10px; box-sizing: border-box; min-height: 100vh;
    display: grid; justify-items: center; align-content: center; gap: 14px;
    font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330;
  }
  .pair { display: flex; gap: 14px; justify-content: center; }
  .col { text-align: center; font-size: 13px; line-height: 1.35; }
  .col b { display: block; margin-bottom: 8px; }
  .bad b { color: #9a3412; } .good b { color: #0f5132; }
  .face {
    position: relative; box-sizing: border-box; margin: 0 auto 8px;
    width: min(38vw, 160px); aspect-ratio: 1;
    border: 4px solid #1d2330; border-radius: 50%; background: #fff;
  }
  .bad .face { border-color: #c2410c; } .good .face { border-color: #15803d; }
  .hand {
    position: absolute; left: 50%; bottom: 50%; width: 3px; height: 42%; margin-left: -1.5px;
    transform-origin: 50% 100%; background: #e0482b; border-radius: 3px;
    transition: transform .5s ease-out;         /* smooth movement between ticks */
  }
  .snap .hand { transition: none; }            /* used once, to jump without animating */
  .pin { position: absolute; left: 50%; top: 50%; width: 10px; height: 10px; margin: -5px; border-radius: 50%; background: #1d2330; }
  button { font: inherit; font-size: 15px; padding: 9px 16px; border-radius: 8px; border: 1px solid #1d2330; background: #1d2330; color: #fff; cursor: pointer; }
  p { margin: 0; font-size: 13px; color: #4b5563; text-align: center; max-width: 340px; }
  code { font: 12px ui-monospace, Consolas, monospace; }
</style>
</head>
<body>
<div class="pair" id="pair">
  <div class="col bad">
    <b>Angle resets</b>
    <div class="face"><div class="hand" id="a"></div><div class="pin"></div></div>
    <code id="angA">0deg</code>
  </div>
  <div class="col good">
    <b>Angle keeps counting</b>
    <div class="face"><div class="hand" id="b"></div><div class="pin"></div></div>
    <code id="angB">0deg</code>
  </div>
</div>
<button id="jump" type="button">Jump to :56</button>
<p>Press the button, then watch the left hand at :59 to :00. It whips back around. The right hand just carries on.</p>

<script>
  const pair = document.getElementById('pair');
  const a = document.getElementById('a');
  const b = document.getElementById('b');
  const angA = document.getElementById('angA');
  const angB = document.getElementById('angB');

  let offset = 0;                                   // lets the button move this clock forward
  const now = () => new Date(Date.now() + offset);

  let turns = 0, last = -1;                         // full laps the hand has made

  function draw(snap) {
    const s = now().getSeconds();
    if (s < last) turns++;                          // seconds went 59 -> 0: one more lap
    last = s;

    const wrapped = s * 6;                          // 0..354, jumps back to 0
    const counting = turns * 360 + s * 6;           // only ever grows

    if (snap) pair.classList.add('snap');
    a.style.transform = 'rotate(' + wrapped + 'deg)';
    b.style.transform = 'rotate(' + counting + 'deg)';
    if (snap) { void pair.offsetWidth; pair.classList.remove('snap'); }

    angA.textContent = wrapped + 'deg';
    angB.textContent = counting + 'deg';
  }

  document.getElementById('jump').addEventListener('click', () => {
    const d = now();
    const ahead = (56 - d.getSeconds() + 60) % 60;  // seconds to add so it reads :56
    offset += ahead * 1000 - d.getMilliseconds();
    turns = 0; last = -1;
    draw(true);
  });

  draw(true);
  setInterval(() => draw(false), 200);              // read the clock often; skipped ticks are harmless
</script>
</body>
</html>
Press Jump to :56 and watch both hands at :59 to :00. The left angle resets to 0. The right angle keeps counting.
The same step on a dial. Resetting the angle sweeps back 354 degrees. A counting angle moves forward 6.
The same step on a dial. Resetting the angle sweeps back 354 degrees. A counting angle moves forward 6.

The fix is to never let the angle fall. Count the laps and add them:

if (s < last) turns++;            // seconds went 59 -> 0
last = s;
hand.style.transform = 'rotate(' + (turns * 360 + s * 6) + 'deg)';

A hand at 360deg looks the same as one at 0deg, so nothing visible changes except that the hand keeps moving forward. For a continuous sweep, redraw on every frame with requestAnimationFrame instead of once a second.

Let CSS do the turning

A clock hand is a turn that repeats, which is what a keyframe animation does. One rule is shared by all three hands, and only the duration changes.

@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.hand   { animation: spin linear infinite; }
.second { animation-duration: 60s; }
.minute { animation-duration: 3600s; }
.hour   { animation-duration: 43200s; }

infinite repeats the animation forever and linear keeps the speed constant. The CSS keyframes guide explains the rest of the syntax.

On its own the animation starts at its first keyframe, so every hand begins at 12. A negative animation-delay starts it part-way through instead. A delay of -15s puts the second hand 15 seconds into its lap.

JavaScript runs once, works out the seconds since midnight, and writes one negative delay per hand. After that the browser does the turning.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CSS-driven analog clock</title>
<style>
  body {
    margin: 0; padding: 12px 10px; box-sizing: border-box; min-height: 100vh;
    display: grid; justify-items: center; align-content: center; gap: 12px;
    font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330;
  }
  .face {
    position: relative; box-sizing: border-box;
    width: min(66vw, 250px); aspect-ratio: 1;
    border: 6px solid #1d2330; border-radius: 50%; background: #fff;
  }
  .mark { position: absolute; inset: 0; }                       /* full-size layer, turned around the center */
  .mark::before { content: ""; position: absolute; left: 50%; top: 2%; width: 1px; height: 3%; margin-left: -.5px; background: #9aa3b2; }
  .mark.five::before { width: 3px; height: 6%; margin-left: -1.5px; background: #1d2330; }
  .num { position: absolute; transform: translate(-50%, -50%); font-weight: 600; font-size: clamp(11px, 4.6vw, 17px); }

  .hand {
    position: absolute; left: 50%; bottom: 50%; transform-origin: 50% 100%;
    border-radius: 4px; background: #1d2330;
    animation: spin linear infinite;            /* the browser does the turning */
    animation-duration: var(--dur);
  }
  @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
  .hour   { --dur: 43200s; width: 6px; height: 26%; margin-left: -3px; }   /* 12 hours */
  .minute { --dur: 3600s;  width: 4px; height: 38%; margin-left: -2px; }   /* 60 minutes */
  .second { --dur: 60s;    width: 2px; height: 42%; margin-left: -1px; background: #e0482b; }
  .ticking .second { animation-timing-function: steps(60); }               /* jump once a second */
  .pin { position: absolute; left: 50%; top: 50%; width: 12px; height: 12px; margin: -6px; border-radius: 50%; background: #e0482b; }

  .row { display: flex; flex-wrap: wrap; gap: 10px 16px; justify-content: center; align-items: center; font-size: 14px; }
  select { font: inherit; padding: 6px 8px; border-radius: 6px; border: 1px solid #c7ccd6; background: #fff; }
  #readout { font: 600 20px ui-monospace, Consolas, monospace; font-variant-numeric: tabular-nums; }
</style>
</head>
<body>
<div class="face" id="face" role="img" aria-label="Analog clock">
  <div class="hand hour" id="hour"></div>
  <div class="hand minute" id="minute"></div>
  <div class="hand second" id="second"></div>
  <div class="pin"></div>
</div>
<div id="readout">--:--:--</div>
<div class="row">
  <label>Time zone
    <select id="zone">
      <option value="">This device</option>
      <option value="Asia/Seoul">Seoul</option>
      <option value="Asia/Kolkata">India (IST)</option>
      <option value="Europe/London">London</option>
      <option value="America/New_York">New York</option>
      <option value="UTC">UTC</option>
    </select>
  </label>
  <label><input type="checkbox" id="ticking"> Ticking second hand</label>
</div>

<script>
  const face = document.getElementById('face');

  // Dial marks: 60 layers, each turned by 6 degrees; every fifth one is bold
  for (let i = 0; i < 60; i++) {
    const m = document.createElement('div');
    m.className = 'mark' + (i % 5 === 0 ? ' five' : '');
    m.style.transform = 'rotate(' + (i * 6) + 'deg)';
    face.prepend(m);
  }
  // Numbers 1-12 placed on a circle with sin and cos
  for (let n = 1; n <= 12; n++) {
    const a = n * 30 * Math.PI / 180;
    const el = document.createElement('div');
    el.className = 'num';
    el.textContent = n;
    el.style.left = (50 + 38 * Math.sin(a)) + '%';
    el.style.top = (50 - 38 * Math.cos(a)) + '%';
    face.append(el);
  }

  const hands = { hour: 43200, minute: 3600, second: 60 };   // seconds per full turn
  const readout = document.getElementById('readout');
  const zone = document.getElementById('zone');
  const ticking = document.getElementById('ticking');

  // Seconds since midnight in the chosen time zone
  function secondsOfDay(tz) {
    const parts = new Intl.DateTimeFormat('en-GB', {
      timeZone: tz || undefined, hourCycle: 'h23',
      hour: 'numeric', minute: 'numeric', second: 'numeric'
    }).formatToParts(new Date());
    const v = {};
    parts.forEach((p) => { v[p.type] = Number(p.value); });
    return v.hour * 3600 + v.minute * 60 + v.second + new Date().getMilliseconds() / 1000;
  }

  // A negative delay starts each hand part-way through its turn
  function setTime() {
    const t = secondsOfDay(zone.value);
    for (const id in hands) {
      const el = document.getElementById(id);
      el.style.animation = 'none';          // restart, so the delay counts from now
      void el.offsetWidth;                  // flush styles
      el.style.animation = '';
      el.style.animationDelay = -(t % hands[id]) + 's';
    }
  }

  function showText() {
    readout.textContent = new Intl.DateTimeFormat('en-GB', {
      timeZone: zone.value || undefined, hourCycle: 'h23',
      hour: '2-digit', minute: '2-digit', second: '2-digit'
    }).format(new Date());
  }

  zone.addEventListener('change', () => { setTime(); showText(); });
  ticking.addEventListener('change', () => face.classList.toggle('ticking', ticking.checked));

  // Respect a visitor who asked for less motion: start with the ticking hand
  if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
    ticking.checked = true;
    face.classList.add('ticking');
  }

  setTime();
  showText();
  setInterval(showText, 250);
</script>
</body>
</html>
The finished clock: a dial made in JavaScript, hands turned by CSS keyframes, a time zone list and a ticking option.

For a ticking hand, give the second hand steps(60) as its timing function. It divides each lap into 60 equal intervals and jumps from one to the next.

The example also checks the prefers-reduced-motion setting with matchMedia. It starts with the ticking hand when the visitor has asked for less motion.

Show the time in another time zone

Date reads the device's clock. To show another city, ask Intl.DateTimeFormat for the parts of the time in a named zone, then use those numbers for the angles.

function secondsOfDay(tz) {
  const parts = new Intl.DateTimeFormat('en-GB', {
    timeZone: tz, hourCycle: 'h23',
    hour: 'numeric', minute: 'numeric', second: 'numeric'
  }).formatToParts(new Date());
  const v = {};
  parts.forEach((p) => { v[p.type] = Number(p.value); });
  return v.hour * 3600 + v.minute * 60 + v.second;
}

The timeZone option takes IANA names such as America/New_York or Europe/London. When it is left out, the device's own zone is used.

For an IST analog clock, use Asia/Kolkata, which Intl prints as India Standard Time. Switch the list in the third example to see the hands and the digital readout move together.

The digital clock guide uses the same idea for a text timezone board, if you want the numbers without a dial.

When it does not work

What you see Cause Fix
Hands never move, or all point to 12 The angle has no unit, such as rotate(90) Write rotate(90deg)
The hand swings around its middle The default origin is 50% 50% transform-origin: 50% 100%
The clock shows 12:00 for a second The first draw waits for the interval Call the function once before setInterval
The hour hand sits on the numbers The minutes are left out of the formula Add minutes * 0.5
The second hand whips back at :00 The angle resets and a transition animates it Count turns, or remove the transition
The second hand skips a tick setInterval can fire later than its delay Read the Date and run every 200 ms
The clock falls behind It adds 6deg per tick instead of reading the time Read the Date on every tick
Others see a different time Date uses each device's own zone Pass a timeZone to Intl
The dial is cut off on a phone The width is a fixed pixel value Use min(70vw, 260px) and aspect-ratio

The setInterval guide explains why timers run late. For sizing on a phone, the page also needs the viewport meta tag.

A screenshot of a clock shows one moment, and it is wrong a second later. The useful thing to send is the clock itself.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to see hands that are moving. Each visitor sees their own device's time unless you set a time zone, as the third example does.

If you change the code later, the same link shows the new version.

Questions people ask

Can I make an analog clock with only CSS?

The hands can turn with CSS alone. A keyframe animation that goes from 0deg to 360deg, repeated forever, is a clock hand. Without JavaScript the hands start where the CSS puts them, for example at 12:00. To start at the real time, a few lines of JavaScript write a negative animation-delay once, as the third example does.

How do I make the second hand tick instead of sweep?

In the CSS version, give the second hand steps(60) as its animation timing function. It splits each lap into 60 equal jumps. In the JavaScript version, set the angle once a second and leave out the transition.

How do I show the time in another time zone, such as IST?

Ask Intl.DateTimeFormat for the hour, minute and second in a named zone, then use those numbers for the angles. India Standard Time is Asia/Kolkata. The third example has a list of zones you can switch between.

Where is the digital version?

The HTML clock widget guide covers a digital clock, a timezone board and a countdown. This guide is about the dial and the hands.

Keep reading