Time told with circular progress bars. Hover or focus on the date, hour, minute, and second to see them individually!
A Pen by Jack-Adam-Mwanzo on CodePen.
<div id="clock" class="progress-clock"> | |
<button class="progress-clock__time-date" data-group="d" type="button"> | |
<small data-unit="w">Sunday</small><br> | |
<span data-unit="mo">January</span> | |
<span data-unit="d">1</span> | |
</button> | |
<button class="progress-clock__time-digit" data-unit="h" data-group="h" type="button">12</button><span class="progress-clock__time-colon">:</span><button class="progress-clock__time-digit" data-unit="m" data-group="m" type="button">00</button><span class="progress-clock__time-colon">:</span><button class="progress-clock__time-digit" data-unit="s" data-group="s" type="button">00</button> | |
<span class="progress-clock__time-ampm" data-unit="ap">AM</span> | |
<svg class="progress-clock__rings" width="256" height="256" viewBox="0 0 256 256"> | |
<defs> | |
<linearGradient id="pc-red" x1="1" y1="0.5" x2="0" y2="0.5"> | |
<stop offset="0%" stop-color="hsl(343,90%,55%)" /> | |
<stop offset="100%" stop-color="hsl(323,90%,55%)" /> | |
</linearGradient> | |
<linearGradient id="pc-yellow" x1="1" y1="0.5" x2="0" y2="0.5"> | |
<stop offset="0%" stop-color="hsl(43,90%,55%)" /> | |
<stop offset="100%" stop-color="hsl(23,90%,55%)" /> | |
</linearGradient> | |
<linearGradient id="pc-blue" x1="1" y1="0.5" x2="0" y2="0.5"> | |
<stop offset="0%" stop-color="hsl(223,90%,55%)" /> | |
<stop offset="100%" stop-color="hsl(203,90%,55%)" /> | |
</linearGradient> | |
<linearGradient id="pc-purple" x1="1" y1="0.5" x2="0" y2="0.5"> | |
<stop offset="0%" stop-color="hsl(283,90%,55%)" /> | |
<stop offset="100%" stop-color="hsl(263,90%,55%)" /> | |
</linearGradient> | |
</defs> | |
<!-- Days of Month --> | |
<g data-units="d"> | |
<circle class="progress-clock__ring" cx="128" cy="128" r="74" fill="none" opacity="0.1" stroke="url(#pc-red)" stroke-width="12" /> | |
<circle class="progress-clock__ring-fill" data-ring="mo" cx="128" cy="128" r="74" fill="none" stroke="url(#pc-red)" stroke-width="12" stroke-dasharray="465 465" stroke-dashoffset="465" stroke-linecap="round" transform="rotate(-90,128,128)" /> | |
</g> | |
<!-- Hours of Day --> | |
<g data-units="h"> | |
<circle class="progress-clock__ring" cx="128" cy="128" r="90" fill="none" opacity="0.1" stroke="url(#pc-yellow)" stroke-width="12" /> | |
<circle class="progress-clock__ring-fill" data-ring="d" cx="128" cy="128" r="90" fill="none" stroke="url(#pc-yellow)" stroke-width="12" stroke-dasharray="565.5 565.5" stroke-dashoffset="565.5" stroke-linecap="round" transform="rotate(-90,128,128)" /> | |
</g> | |
<!-- Minutes of Hour --> | |
<g data-units="m"> | |
<circle class="progress-clock__ring" cx="128" cy="128" r="106" fill="none" opacity="0.1" stroke="url(#pc-blue)" stroke-width="12" /> | |
<circle class="progress-clock__ring-fill" data-ring="h" cx="128" cy="128" r="106" fill="none" stroke="url(#pc-blue)" stroke-width="12" stroke-dasharray="666 666" stroke-dashoffset="666" stroke-linecap="round" transform="rotate(-90,128,128)" /> | |
</g> | |
<!-- Seconds of Minute --> | |
<g data-units="s"> | |
<circle class="progress-clock__ring" cx="128" cy="128" r="122" fill="none" opacity="0.1" stroke="url(#pc-purple)" stroke-width="12" /> | |
<circle class="progress-clock__ring-fill" data-ring="m" cx="128" cy="128" r="122" fill="none" stroke="url(#pc-purple)" stroke-width="12" stroke-dasharray="766.5 766.5" stroke-dashoffset="766.5" stroke-linecap="round" transform="rotate(-90,128,128)" /> | |
</g> | |
</svg> | |
</div> |
Time told with circular progress bars. Hover or focus on the date, hour, minute, and second to see them individually!
A Pen by Jack-Adam-Mwanzo on CodePen.
window.addEventListener("DOMContentLoaded",() => { | |
const clock = new ProgressClock("#clock"); | |
}); | |
class ProgressClock { | |
constructor(qs) { | |
this.el = document.querySelector(qs); | |
this.time = 0; | |
this.updateTimeout = null; | |
this.ringTimeouts = []; | |
this.update(); | |
} | |
getDayOfWeek(day) { | |
switch (day) { | |
case 1: | |
return "Monday"; | |
case 2: | |
return "Tuesday"; | |
case 3: | |
return "Wednesday"; | |
case 4: | |
return "Thursday"; | |
case 5: | |
return "Friday"; | |
case 6: | |
return "Saturday"; | |
default: | |
return "Sunday"; | |
} | |
} | |
getMonthInfo(mo,yr) { | |
switch (mo) { | |
case 1: | |
return { name: "February", days: yr % 4 === 0 ? 29 : 28 }; | |
case 2: | |
return { name: "March", days: 31 }; | |
case 3: | |
return { name: "April", days: 30 }; | |
case 4: | |
return { name: "May", days: 31 }; | |
case 5: | |
return { name: "June", days: 30 }; | |
case 6: | |
return { name: "July", days: 31 }; | |
case 7: | |
return { name: "August", days: 31 }; | |
case 8: | |
return { name: "September", days: 30 }; | |
case 9: | |
return { name: "October", days: 31 }; | |
case 10: | |
return { name: "November", days: 30 }; | |
case 11: | |
return { name: "December", days: 31 }; | |
default: | |
return { name: "January", days: 31 }; | |
} | |
} | |
update() { | |
this.time = new Date(); | |
if (this.el) { | |
// date and time | |
const dayOfWeek = this.time.getDay(); | |
const year = this.time.getFullYear(); | |
const month = this.time.getMonth(); | |
const day = this.time.getDate(); | |
const hr = this.time.getHours(); | |
const min = this.time.getMinutes(); | |
const sec = this.time.getSeconds(); | |
const dayOfWeekName = this.getDayOfWeek(dayOfWeek); | |
const monthInfo = this.getMonthInfo(month,year); | |
const m_progress = sec / 60; | |
const h_progress = (min + m_progress) / 60; | |
const d_progress = (hr + h_progress) / 24; | |
const mo_progress = ((day - 1) + d_progress) / monthInfo.days; | |
const units = [ | |
{ | |
label: "w", | |
value: dayOfWeekName | |
}, | |
{ | |
label: "mo", | |
value: monthInfo.name, | |
progress: mo_progress | |
}, | |
{ | |
label: "d", | |
value: day, | |
progress: d_progress | |
}, | |
{ | |
label: "h", | |
value: hr > 12 ? hr - 12 : hr, | |
progress: h_progress | |
}, | |
{ | |
label: "m", | |
value: min < 10 ? "0" + min : min, | |
progress: m_progress | |
}, | |
{ | |
label: "s", | |
value: sec < 10 ? "0" + sec : sec | |
}, | |
{ | |
label: "ap", | |
value: hr > 12 ? "PM" : "AM" | |
} | |
]; | |
// flush out the timeouts | |
this.ringTimeouts.forEach(t => { | |
clearTimeout(t); | |
}); | |
this.ringTimeouts = []; | |
// update the display | |
units.forEach(u => { | |
// rings | |
const ring = this.el.querySelector(`[data-ring="${u.label}"]`); | |
if (ring) { | |
const strokeDashArray = ring.getAttribute("stroke-dasharray"); | |
const fill360 = "progress-clock__ring-fill--360"; | |
if (strokeDashArray) { | |
// calculate the stroke | |
const circumference = +strokeDashArray.split(" ")[0]; | |
const strokeDashOffsetPct = 1 - u.progress; | |
ring.setAttribute( | |
"stroke-dashoffset", | |
strokeDashOffsetPct * circumference | |
); | |
// add the fade-out transition, then remove it | |
if (strokeDashOffsetPct === 1) { | |
ring.classList.add(fill360); | |
this.ringTimeouts.push( | |
setTimeout(() => { | |
ring.classList.remove(fill360); | |
}, 600) | |
); | |
} | |
} | |
} | |
// digits | |
const unit = this.el.querySelector(`[data-unit="${u.label}"]`); | |
if (unit) | |
unit.innerText = u.value; | |
}); | |
} | |
clearTimeout(this.updateTimeout); | |
this.updateTimeout = setTimeout(this.update.bind(this),1e3); | |
} | |
} |
* { | |
border: 0; | |
box-sizing: border-box; | |
margin: 0; | |
padding: 0; | |
} | |
:root { | |
--hue: 223; | |
--bg: hsl(var(--hue),10%,90%); | |
--fg: hsl(var(--hue),10%,10%); | |
font-size: calc(16px + (24 - 16) * (100vw - 320px) / (1280 - 320)); | |
} | |
body, button { | |
color: var(--fg); | |
font: 1em/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
} | |
body { | |
background-color: var(--bg); | |
height: 100vh; | |
display: grid; | |
place-items: center; | |
} | |
.progress-clock { | |
display: grid; | |
justify-content: center; | |
align-content: center; | |
position: relative; | |
text-align: center; | |
width: 16em; | |
height: 16em; | |
} | |
.progress-clock__time-date, | |
.progress-clock__time-digit, | |
.progress-clock__time-colon, | |
.progress-clock__time-ampm { | |
transition: color 0.2s linear; | |
-webkit-user-select: none; | |
-moz-user-select: none; | |
user-select: none; | |
} | |
.progress-clock__time-date, | |
.progress-clock__time-digit { | |
background: transparent; | |
} | |
.progress-clock__time-date, | |
.progress-clock__time-ampm { | |
grid-column: 1 / 6; | |
} | |
.progress-clock__time-date { | |
font-size: 0.75em; | |
line-height: 1.33; | |
} | |
.progress-clock__time-digit, | |
.progress-clock__time-colon { | |
font-size: 2em; | |
font-weight: 400; | |
grid-row: 2; | |
} | |
.progress-clock__time-colon { | |
line-height: 1.275; | |
} | |
.progress-clock__time-ampm { | |
cursor: default; | |
grid-row: 3; | |
} | |
.progress-clock__rings { | |
display: block; | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
z-index: -1; | |
} | |
.progress-clock__ring { | |
opacity: 0.1; | |
} | |
.progress-clock__ring-fill { | |
transition: | |
opacity 0s 0.3s linear, | |
stroke-dashoffset 0.3s ease-in-out; | |
} | |
.progress-clock__ring-fill--360 { | |
opacity: 0; | |
stroke-dashoffset: 0; | |
transition-duration: 0.3s; | |
} | |
[data-group]:focus { | |
outline: transparent; | |
} | |
[data-units] { | |
transition: opacity 0.2s linear; | |
} | |
[data-group="d"]:focus, | |
[data-group="d"]:hover { | |
color: hsl(333,90%,55%); | |
} | |
[data-group="h"]:focus, | |
[data-group="h"]:hover { | |
color: hsl(33,90%,55%); | |
} | |
[data-group="m"]:focus, | |
[data-group="m"]:hover { | |
color: hsl(213,90%,55%); | |
} | |
[data-group="s"]:focus, | |
[data-group="s"]:hover { | |
color: hsl(273,90%,55%); | |
} | |
[data-group]:focus ~ .progress-clock__rings [data-units], | |
[data-group]:hover ~ .progress-clock__rings [data-units] { | |
opacity: 0.2; | |
} | |
[data-group="d"]:focus ~ .progress-clock__rings [data-units="d"], | |
[data-group="d"]:hover ~ .progress-clock__rings [data-units="d"], | |
[data-group="h"]:focus ~ .progress-clock__rings [data-units="h"], | |
[data-group="h"]:hover ~ .progress-clock__rings [data-units="h"], | |
[data-group="m"]:focus ~ .progress-clock__rings [data-units="m"], | |
[data-group="m"]:hover ~ .progress-clock__rings [data-units="m"], | |
[data-group="s"]:focus ~ .progress-clock__rings [data-units="s"], | |
[data-group="s"]:hover ~ .progress-clock__rings [data-units="s"] { | |
opacity: 1; | |
} | |
/* Dark theme */ | |
@media (prefers-color-scheme: dark) { | |
:root { | |
--bg: hsl(var(--hue),10%,10%); | |
--fg: hsl(var(--hue),10%,90%); | |
} | |
.progress-clock__ring { | |
opacity: 0.2; | |
} | |
} |