Current time:
Each digit of the current time is drawn as a 4×6 grid of mini clocks. The two hands on each cell rotate to form corners, edges, or straights, mapping to the box-drawing characters that make up the digit shape.
The digit patterns are stored as flat arrays of 24 box-drawing characters (┌ ┐ └ ┘ - |), and each character maps to a pair of rotation angles for the two hands. Every second, the angles update and CSS transitions do the rest.
astro
<section
class="clock-clock"
aria-label="Digital time displayed using analog clock hands"
>
<div class="sr-only" aria-live="polite">
Current time: <span id="accessible-time"></span>
</div>
<div id="clock-container" aria-hidden="true">
{
["hours", "minutes", "seconds"].map((unit) => (
<div class={`unit ${unit}`}>
{[0, 1].map(() => (
<div class="digit">
{Array.from({ length: 24 }).map(() => (
<div class="cell">
<div class="hand" />
<div class="hand" />
</div>
))}
</div>
))}
</div>
))
}
</div>
</section>
<script>
// prettier-ignore
const DIGITS = {
"0": [
"┌", "-", "-", "┐",
"|", "┌", "┐", "|",
"|", "|", "|", "|",
"|", "|", "|", "|",
"|", "└", "┘", "|",
"└", "-", "-", "┘",
],
"1": [
"┌", "-", "┐", " ",
"└", "┐", "|", " ",
" ", "|", "|", " ",
" ", "|", "|", " ",
"┌", "┘", "└", "┐",
"└", "-", "-", "┘",
],
"2": [
"┌", "-", "-", "┐",
"└", "-", "┐", "|",
"┌", "-", "┘", "|",
"|", "┌", "-", "┘",
"|", "└", "-", "┐",
"└", "-", "-", "┘",
],
"3": [
"┌", "-", "-", "┐",
"└", "-", "┐", "|",
" ", "┌", "┘", "|",
" ", "└", "┐", "|",
"┌", "-", "┘", "|",
"└", "-", "-", "┘",
],
"4": [
"┌", "┐", "┌", "┐",
"|", "|", "|", "|",
"|", "└", "┘", "|",
"└", "-", "┐", "|",
" ", " ", "|", "|",
" ", " ", "└", "┘",
],
"5": [
"┌", "-", "-", "┐",
"|", "┌", "-", "┘",
"|", "└", "-", "┐",
"└", "-", "┐", "|",
"┌", "-", "┘", "|",
"└", "-", "-", "┘",
],
"6": [
"┌", "-", "-", "┐",
"|", "┌", "-", "┘",
"|", "└", "-", "┐",
"|", "┌", "┐", "|",
"|", "└", "┘", "|",
"└", "-", "-", "┘",
],
"7": [
"┌", "-", "-", "┐",
"└", "-", "┐", "|",
" ", " ", "|", "|",
" ", " ", "|", "|",
" ", " ", "|", "|",
" ", " ", "└", "┘",
],
"8": [
"┌", "-", "-", "┐",
"|", "┌", "┐", "|",
"|", "└", "┘", "|",
"|", "┌", "┐", "|",
"|", "└", "┘", "|",
"└", "-", "-", "┘",
],
"9": [
"┌", "-", "-", "┐",
"|", "┌", "┐", "|",
"|", "└", "┘", "|",
"└", "-", "┐", "|",
"┌", "-", "┘", "|",
"└", "-", "-", "┘",
]
}
const rotation = {
" ": [135, 135],
"┘": [180, 270],
"└": [0, 270],
"┐": [90, 180],
"┌": [0, 90],
"-": [0, 180],
"|": [90, 270],
};
function updateClock() {
const now = new Date();
const hours = now.getHours().toString().padStart(2, "0");
const minutes = now.getMinutes().toString().padStart(2, "0");
const seconds = now.getSeconds().toString().padStart(2, "0");
const timeString = hours + minutes + seconds;
const digitElements = document.querySelectorAll(".digit");
const accessibleTime = document.getElementById("accessible-time");
if (accessibleTime) {
accessibleTime.textContent = `${hours}:${minutes}:${seconds}`;
}
timeString.split("").forEach((char, digitIndex) => {
const pattern = DIGITS[char as keyof typeof DIGITS];
if (!pattern) return;
const digitEl = digitElements[digitIndex];
if (!digitEl) return;
const cells = digitEl.querySelectorAll(".cell");
cells.forEach((cell, cellIndex) => {
const symbol = pattern[cellIndex];
const angles = rotation[symbol as keyof typeof rotation] || [135, 135];
const hands = cell.querySelectorAll(".hand");
if (hands[0])
(hands[0] as HTMLElement).style.rotate = `${angles[0]}deg`;
if (hands[1])
(hands[1] as HTMLElement).style.rotate = `${angles[1]}deg`;
});
});
}
updateClock();
setInterval(updateClock, 1000);
</script>
<style>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
section {
border: 1px solid var(--border);
border-radius: var(--radius);
padding: clamp(16px, 4vw, 32px);
margin-bottom: 0px;
overflow: hidden;
position: relative;
}
#clock-container {
width: 100%;
display: flex;
gap: 4.5%; /* Relative gap between units */
justify-content: center;
align-items: center;
}
.unit {
display: flex;
gap: 1.5%; /* Relative gap between digits */
flex: 1;
max-width: 30%;
}
.digit {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1px;
flex: 1;
}
.cell {
width: 100%;
aspect-ratio: 1;
border: 1px solid var(--border-muted);
border-radius: 50%;
background-color: var(--bg-soft);
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.hand {
width: 50%;
height: 1.5px;
background-color: var(--text-muted);
position: absolute;
left: 50%;
transform-origin: center left;
transition: rotate 500ms cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 1px;
}
</style>