DEV Community

Cover image for Clocks and Watches in CSS
Mads Stoumann
Mads Stoumann

Posted on Edited on

Clocks and Watches in CSS

A couple of years ago, when CSS trigonometry functions became baseline, I wrote an article about them. One of the examples I did, was a CSS-only analog clock:

Since then, CSS has introduced a bunch of new features — one being offset-path, which is perfect for creating indices on a clock (I sound like an horology expert, but I Googled that).

So, without further ado, let's expand my old example with some more, cool features! We'll wrap it within a Web Component for easier customization, but you can stick with CSS-only, if you want.


First, we set up a simple grid, divided into 3 rows:

Main Grid

:host {
  aspect-ratio: 1;
  background: #f2f2f2;
  border-radius: 50%;
  display: grid;
  grid-template-rows: repeat(3, 1fr);
}
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

The indices are a bunch of <li> elements within a <ul>, using offset-distance / path to place them around the circle:

li {
  display: inline-block;
  list-style: none;
  offset-distance: var(--_d);
  offset-path: content-box;
  width: fit-content;
}
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

Each <li> has a degree (actually a percentage), defined in the --_d custom property:

<li style="--_d:0%">|</li>
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

This gets us:

Indices

By default, offset-rotate automatically rotates elements to follow the path direction. This behavior is exactly what we need for the indices, so we don't need to set any additional rotation.

Now, for the numerals, we'll also use <li>, but this time within an ordered list, <ol>:

<ol>
  <li style="--_d:300deg">1</li>
</ol>
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

We'll use cos() and sin() to place the numerals, like in my original example.

li {
  --_r: calc((100% - 15cqi) / 2);
  --_x: calc(var(--_r) + (var(--_r) * cos(var(--_d))));
  --_y: calc(var(--_r) + (var(--_r) * sin(var(--_d))));
  aspect-ratio: 1;
  display: grid;
  left: var(--_x);
  place-content: center;
  position: absolute;
  top: var(--_y);
  width: 15cqi;
}
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

And we get:

Numerals

Now, let's create the markup for the hands and date. The cap will be added as a pseudo-element. I had a hard time trying to wrap my head around what good, semantic markup would be here? I gave up, and just used a bunch of <div>s 😄

<nav part="hands">
  <div part="seconds"></div>
  <div part="minutes"></div>
  <div part="hours"></div>
  <time part="date"></time>
</nav>
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

We position the <nav> in the middle row of the main grid, and create a 3-column grid:

:host::part(hands) {
  display: grid;
  grid-area: 2 / 1 / 3 / 1;
  grid-template-columns: repeat(3, 1fr);
}
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

This gives us:
Hands

Finally, we place the label at the top center of the last row of the main grid:

Label


Animating the hands

To animate the hands, we just need a single animation:

@keyframes turn {
  to { transform: rotate(1turn); }
}
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

However, it needs to be called in 3 very distinct ways:

:host::part(hours) {
  animation: turn 43200s linear infinite;
  animation-delay: var(--_dh, 0ms);
}
:host::part(minutes) {
  animation: turn 3600s steps(60, end) infinite;
  animation-delay: var(--_dm, 0ms);
}
:host::part(seconds) {
  animation: turn 60s linear infinite;
  animation-delay: var(--_ds, 0ms);
}
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

And that's it! ... if you don't mind the clock always starting at noon!

To initialize the clock with the actual time, we need to update the delay properties: --_dh, --_dm and --_ds — and for that, we need a small snippet of JavaScript:

const time = new Date();
const hour = -3600 * (time.getHours() % 12);
const mins = -60 * time.getMinutes();
app.style.setProperty('--_dm', `${mins}s`);
app.style.setProperty('--_dh', `${(hour+mins)}s`);
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

Variants

Styling variants is dead simple (see the final demo at the end of the article).

How about a SAIKO:

SAIKO

Or a ROBEX (sorry for my unimaginative names!):

ROBEX

... or how about some really colorful examples:

Burmese, Thai and Indian

The latter can, of course, be done by adding the labels manually, but if we wrap it in a web component, it becomes a bit easier to maintain:

<analog-clock
  label="မြန်မာ"
  system="mymr"
  timezone="+6.5"
  class="burmese"
  indices
  marker="•">
</analog-clock>

<analog-clock
  label="ประเทศไทย"
  system="thai"
  timezone="+7"
  class="thai"
  indices
  marker="·"
  marker-hour="•">
</analog-clock>

<analog-clock
  label="अरुणाचल"
  system="wcho"
  timezone="+5.5"
  class="indian">
</analog-clock>
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

Let's look into that.


Web Component

Wrapping the code in a <analog-clock> web component offers a simple way to add an analog clock to your web projects. It's customizable through various attributes and CSS custom properties.

Installation & Usage

Install via npm:

npm i @browser.style/analog-clock
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

Or use directly via CDN:

<script src="https://browser.style/ui/analog-clock/index.js" type="module"></script>
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

Then, simply add the component to your HTML:

<analog-clock></analog-clock>
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

Basic Examples

Here are some common use cases:

<!-- Simple clock for New York time -->
<analog-clock 
  label="New York" 
  timezone="-4">
</analog-clock>

<!-- Clock with date display and minute markers -->
<analog-clock 
  indices 
  date="day month" 
  label="Current Time">
</analog-clock>

<!-- Clock with custom markers and Roman numerals -->
<analog-clock 
  indices="hours"
  system="roman"
  marker="•"
  marker-hour="●"
  label="Roma">
</analog-clock>
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

Styling Examples

The component can be styled using CSS custom properties:

/* Gold luxury theme */
.luxury {
  --analog-clock-bg: radial-gradient(
    circle at 50% 50%,
    #f4e5c3 50%,
    #e2ca7d 51%,
    #5c4d28 95%
  );
  --analog-clock-c: #2a2317;
  --analog-clock-ff: "Didot", serif;
  --analog-clock-second: #8b0000;
  --analog-clock-cap: #403428;
}

/* Minimalist theme */
.minimal {
  --analog-clock-bg: #fff;
  --analog-clock-c: #333;
  --analog-clock-indices-c: #ddd;
  --analog-clock-second: #ff4444;
  --analog-clock-cap-sz: 4cqi;
}
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

Number Systems

The system attribute supports various number systems, as we saw in the colorful examples earlier:

<analog-clock system="mymr"></analog-clock>
<analog-clock system="thai"></analog-clock>
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

Timezone Support

You can display different timezones using the timezone attribute:

<analog-clock label="New York" timezone="-4"></analog-clock>
<analog-clock label="London" timezone="0"></analog-clock>
<analog-clock label="Tokyo" timezone="+9"></analog-clock>
<analog-clock label="Mumbai" timezone="+5.5"></analog-clock>
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

Attributes

  • date: Display date. Values: "day", "month", "year" or any combination
  • indices: Show tick marks. Values: empty (60 marks) or "hours" (12 marks)
  • label: Text label below the clock
  • marker: Character used for indices (default: "|")
  • marker-hour: Character used for hour indices (defaults to marker value)
  • numerals: Number of numerals to display (1-12, default: 12)
  • steps: Use stepping animation for seconds hand
  • system: Number system. Values: "roman", "romanlow", or any valid Intl numberingSystem
  • timezone: UTC offset in hours (e.g., "-4", "+1", "+5.5")

Demo

Here's a Codepen with all the clocks and watches, we've coded:

Now go teach kids how to read an analog clock!

Top comments (41)

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
crosschainer profile image
crosschainer •

sick

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
artydev profile image
artydev • • Edited

Awesome, thank you ;-)

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann •

Thanks!

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
nat1craft profile image
Nat1Craft • • Edited

First off, great work! Love the cleverness of it all and the styling. Learned alot. Thanks for designing and posting this.

Second, I know this is an older post but figured I would point this out anyway. It looks like there is a small flaw in the minute/hourly animations. The animation for the minute hand (and hour) move NOT at the top of the minute (or hour), but they move dependent upon when you loaded/refreshed the page. Meaning? If you load/refresh the page at 10:05:17 then the minute hand will not move until 10:06:17 when it should have moved at 10:06:00. The same holds true for the hour hand.

Luckily the fix is simple. Just change the UpdateClock function to determine the proper offsets. There are two places below that need to be changed. First, calcuate the animation offsets. Second, make a tweak to the existing code that set the offsets, but use them directly (there was some fuzzy addition here previously)

updateClock() {
        const time = new Date();
        const tzOffset = parseFloat(this.getAttribute('timezone') || '0');

        // Convert to UTC first, then add timezone offset
        const utc = time.getTime() + (time.getTimezoneOffset() * 60000);
        const tzTime = new Date(utc + (3600000 * tzOffset));

        // FIRST: The animation offset it cumulative!
        const secs = -tzTime.getSeconds();
        const mins = -60 * tzTime.getMinutes() + secs;
        const hour = -3600 * (tzTime.getHours() % 12) + mins;

        // Update date display
        this.#date.textContent = this.#formatDate(tzTime);

        // SECOND: Use the animation offsets directly
        this.style.setProperty('--_dh', `${(hour)}s`);
        this.style.setProperty('--_dm', `${mins}s`);
        this.style.setProperty('--_ds', `${secs}s`);
}
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann •

Great finding, thank you!

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
stantheman70 profile image
StanTheMan70 •

Thank you! This is awesome! However, there's a small issue causing the script to work incorrectly with fractional time zones. For example, when the timezone is set to +9.5, the clock displays the time half an hour earlier than it should.
To fix this, simply replace parseInt with** parseFloat** when retrieving the timezone value from the element attribute. This will ensure that fractional offsets are handled correctly.
This error exists even in the CDN.

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann • • Edited

Ah, didn't think about that — thanks for letting me know!
I've added this method, so it rounds the timezone to nearest quarter of an hour:

#roundTzOffset(offset) {
  return Math.round((parseFloat(offset) || 0) * 4) / 4
};
🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung 🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung

And since you're called StanTheMan70 — here's Stan The Man and me, 25 years ago ;-)

StanTheMan

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
stantheman70 profile image
StanTheMan70 •

Image description

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
deathcrafter profile image
Shaktijeet Sahoo •

Holy CSS!

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
plutonium239 profile image
plutonium239 •

Nice, but you can just name the variable --d without the underscore, neater.

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann •

That's to indicate the variable should be considerend "private".

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
michael_phillips_356cb1ff profile image
Michael Phillips •

So nice!

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann •

Thanks!

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
zoujia profile image
zoujia •

COOL👍MARK❤

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann •

Thanks!

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
mahdijazini profile image
Mahdi Jazini •

Good Job !

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann •

Thanks!

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
samirhembrom profile image
SAMIR HEMBROM •

Absolutely amazing

🔥 Jadwal & Hasil: madsstoumann/clocks and watches in css mm - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann •

Thanks!