DEV Community

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

Posted on • Edited on

72 7 7 6 11

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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

<li style="--_d:0%">|</li>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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); }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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`);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Or use directly via CDN:

<script src="https://browser.style/ui/analog-clock/index.js" type="module"></script>
Enter fullscreen mode Exit fullscreen mode

Then, simply add the component to your HTML:

<analog-clock></analog-clock>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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!

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (40)

Collapse
 
crosschainer profile image
crosschainer

sick

Collapse
 
artydev profile image
artydev • Edited

Awesome, thank you ;-)

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!

Collapse
 
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.

Collapse
 
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
};
Enter fullscreen mode Exit fullscreen mode

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

StanTheMan

Collapse
 
stantheman70 profile image
StanTheMan70

Image description

Collapse
 
deathcrafter profile image
Shaktijeet Sahoo

Holy CSS!

Collapse
 
plutonium239 profile image
plutonium239

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

Collapse
 
madsstoumann profile image
Mads Stoumann

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

Collapse
 
keith_murimi profile image
Keith Murimi

This is timeless CSS at it's best

Collapse
 
madsstoumann profile image
Mads Stoumann

Thank you!

Collapse
 
mileswk profile image
MilesWK

Wow! Very interesting! Clocks are hard! Thanks for sharing!

Collapse
 
madsstoumann profile image
Mads Stoumann

Thank you!

Collapse
 
bigol profile image
José Santos Silva

Great work thanks.

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!

Collapse
 
mahdijazini profile image
Mahdi Jazini

Good Job !

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!

Collapse
 
michael_phillips_356cb1ff profile image
Michael Phillips

So nice!

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

👋 Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay