DEV Community

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

Posted 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);
}
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!

Top comments (33)

Collapse
 
crosschainer profile image
crosschainer

sick

Collapse
 
artydev profile image
artydev • Edited

Awesome, thank you ;-)

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!

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
 
mileswk profile image
MilesWK

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

Collapse
 
madsstoumann profile image
Mads Stoumann

Thank you!

Collapse
 
muhayminbinmehmood profile image
Muhaymin Bin Mehmood

Great, It seems awesome 🙌

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!

Collapse
 
bigol profile image
José Santos Silva

Great work thanks.

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!

Collapse
 
michael_phillips_356cb1ff profile image
Michael Phillips

So nice!

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!

Collapse
 
madhurima_rawat profile image
Madhurima Rawat

This looks so good🌟Thanks for sharing!

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!

Collapse
 
samirhembrom profile image
SAMIR HEMBROM

Absolutely amazing

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!