10:08London · GMT
Clock
import { useEffect, useRef, useState } from 'react';
const CITIES = [
{ name: 'London', tz: 'Europe/London' },
{ name: 'Tokyo', tz: 'Asia/Tokyo' },
{ name: 'New York', tz: 'America/New_York' },
];
const S = 112;
const C = S / 2;
const css = `
.card-clock { box-sizing: border-box; width: 240px; padding: 14px; border-radius: 16px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.card-clock__top { display: flex; align-items: center; gap: 14px; }
.card-clock__face { flex: none; width: ${S}px; height: ${S}px; }
.card-clock__dial { fill: light-dark(#ebedf0, #232529); }
.card-clock__tick { stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.5; stroke-linecap: round; }
.card-clock__tick--hour { stroke: light-dark(#50535b, #c6c8cd); stroke-width: 2.5; }
.card-clock__hand { stroke-linecap: round; transform-box: view-box; transform-origin: ${C}px ${C}px; }
.card-clock__hand--h { stroke: light-dark(#373a40, #f1f2f3); stroke-width: 4; transition: transform 0.9s cubic-bezier(0.3, 1.35, 0.5, 1); }
.card-clock__hand--m { stroke: light-dark(#373a40, #f1f2f3); stroke-width: 2.5; transition: transform 0.9s cubic-bezier(0.3, 1.35, 0.5, 1) 0.05s; }
.card-clock__hand--s { stroke: light-dark(#5f626a, #a7abb2); stroke-width: 1.2; transition: transform 0.25s cubic-bezier(0.3, 2, 0.5, 1); }
.card-clock__pin { fill: light-dark(#373a40, #f1f2f3); }
.card-clock__cities { display: grid; gap: 4px; flex: 1; }
.card-clock__city { height: 28px; padding: 0 10px; border: 0; border-radius: 8px; background: none; color: light-dark(#5f626a, #a7abb2); font: inherit; font-size: 12px; text-align: left; cursor: pointer; transition: background-color 0.2s, color 0.2s; }
.card-clock__city[aria-pressed='true'] { background: light-dark(#ebedf0, #232529); color: light-dark(#373a40, #f1f2f3); font-weight: 600; }
.card-clock__city:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 1px; }
.card-clock__read { display: flex; align-items: baseline; gap: 8px; margin: 10px 0 0; }
.card-clock__read b { font-size: 22px; font-weight: 700; color: light-dark(#373a40, #f1f2f3); font-variant-numeric: tabular-nums; }
.card-clock__read span { font-size: 11px; color: light-dark(#5f626a, #a7abb2); }
@media (prefers-reduced-motion: reduce) {
.card-clock__hand--h, .card-clock__hand--m, .card-clock__hand--s { transition: none; }
}
`;
function timeIn(tz: string, now: Date) {
const parts = new Intl.DateTimeFormat('en-GB', { timeZone: tz, hour: 'numeric', minute: 'numeric', second: 'numeric', hourCycle: 'h23', timeZoneName: 'shortOffset' }).formatToParts(now);
const get = (t: string) => parts.find((p) => p.type === t)?.value ?? '0';
return { h: +get('hour'), m: +get('minute'), s: +get('second'), zone: get('timeZoneName') };
}
// Keep an angle within half a turn of the last one, so a hand always takes the short way round.
const near = (prev: number, a: number) => a + 360 * Math.round((prev - a) / 360);
export function ClockCard() {
const [city, setCity] = useState(0);
// The first render is a fixed time so the server and the browser agree; the live time takes over after.
const [now, setNow] = useState<Date | null>(null);
const last = useRef({ h: 0, m: 0, s: 0 });
useEffect(() => {
let id = 0;
const tick = () => {
setNow(new Date());
id = window.setTimeout(tick, 1000 - (Date.now() % 1000));
};
tick();
return () => clearTimeout(id);
}, []);
const t = now ? timeIn(CITIES[city].tz, now) : { h: 10, m: 8, s: 36, zone: 'GMT' };
const q = last.current;
q.h = near(q.h, ((t.h % 12) + t.m / 60) * 30);
q.m = near(q.m, (t.m + t.s / 60) * 6);
q.s = near(q.s, t.s * 6);
const hand = (cls: string, a: number, len: number) => <line className={`card-clock__hand card-clock__hand--${cls}`} x1={C} y1={C + 8} x2={C} y2={C - len} style={{ transform: `rotate(${a}deg)` }} />;
return (
<>
<style>
{css}
</style>
<article className="card-clock">
<div className="card-clock__top">
<svg className="card-clock__face" viewBox={`0 0 ${S} ${S}`} aria-hidden="true">
<circle className="card-clock__dial" cx={C} cy={C} r={C - 1} />
{Array.from({ length: 12 }, (_, i) => (
<line
key={i}
className={`card-clock__tick${i % 3 === 0 ? ' card-clock__tick--hour' : ''}`}
x1={C}
y1={8}
x2={C}
y2={i % 3 === 0 ? 16 : 13}
transform={`rotate(${i * 30} ${C} ${C})`}
/>
))}
{hand('h', q.h, 28)}
{hand('m', q.m, 42)}
{hand('s', q.s, 46)}
<circle className="card-clock__pin" cx={C} cy={C} r={3.5} />
</svg>
<div className="card-clock__cities" role="group" aria-label="City">
{CITIES.map((c, i) => (
<button key={c.name} type="button" className="card-clock__city" aria-pressed={city === i} onClick={() => setCity(i)}>
{c.name}
</button>
))}
</div>
</div>
<p className="card-clock__read">
<b>
{String(t.h).padStart(2, '0')}:{String(t.m).padStart(2, '0')}
</b>
<span>
{CITIES[city].name} · {t.zone}
</span>
</p>
</article>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardClock(el) on the mounted element instead.
<article class="card-clock">
<div class="card-clock__top">
<svg class="card-clock__face" viewBox="0 0 112 112" aria-hidden="true">
<circle class="card-clock__dial" cx="56" cy="56" r="55"></circle>
<line class="card-clock__tick card-clock__tick--hour" x1="56" y1="8" x2="56" y2="16" transform="rotate(0 56 56)"></line>
<line class="card-clock__tick" x1="56" y1="8" x2="56" y2="13" transform="rotate(30 56 56)"></line>
<line class="card-clock__tick" x1="56" y1="8" x2="56" y2="13" transform="rotate(60 56 56)"></line>
<line class="card-clock__tick card-clock__tick--hour" x1="56" y1="8" x2="56" y2="16" transform="rotate(90 56 56)"></line>
<line class="card-clock__tick" x1="56" y1="8" x2="56" y2="13" transform="rotate(120 56 56)"></line>
<line class="card-clock__tick" x1="56" y1="8" x2="56" y2="13" transform="rotate(150 56 56)"></line>
<line class="card-clock__tick card-clock__tick--hour" x1="56" y1="8" x2="56" y2="16" transform="rotate(180 56 56)"></line>
<line class="card-clock__tick" x1="56" y1="8" x2="56" y2="13" transform="rotate(210 56 56)"></line>
<line class="card-clock__tick" x1="56" y1="8" x2="56" y2="13" transform="rotate(240 56 56)"></line>
<line class="card-clock__tick card-clock__tick--hour" x1="56" y1="8" x2="56" y2="16" transform="rotate(270 56 56)"></line>
<line class="card-clock__tick" x1="56" y1="8" x2="56" y2="13" transform="rotate(300 56 56)"></line>
<line class="card-clock__tick" x1="56" y1="8" x2="56" y2="13" transform="rotate(330 56 56)"></line>
<line class="card-clock__hand card-clock__hand--h" x1="56" y1="64" x2="56" y2="28" style="transform:rotate(-56deg)"></line>
<line class="card-clock__hand card-clock__hand--m" x1="56" y1="64" x2="56" y2="14" style="transform:rotate(51.6deg)"></line>
<line class="card-clock__hand card-clock__hand--s" x1="56" y1="64" x2="56" y2="10" style="transform:rotate(-144deg)"></line>
<circle class="card-clock__pin" cx="56" cy="56" r="3.5"></circle>
</svg>
<div class="card-clock__cities" role="group" aria-label="City">
<button type="button" class="card-clock__city" aria-pressed="true">London</button>
<button type="button" class="card-clock__city" aria-pressed="false">Tokyo</button>
<button type="button" class="card-clock__city" aria-pressed="false">New York</button>
</div>
</div>
<p class="card-clock__read">
<b>10:08</b>
<span>London · GMT</span>
</p>
</article>.card-clock { box-sizing: border-box; width: 240px; padding: 14px; border-radius: 16px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.card-clock__top { display: flex; align-items: center; gap: 14px; }
.card-clock__face { flex: none; width: 112px; height: 112px; }
.card-clock__dial { fill: light-dark(#ebedf0, #232529); }
.card-clock__tick { stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.5; stroke-linecap: round; }
.card-clock__tick--hour { stroke: light-dark(#50535b, #c6c8cd); stroke-width: 2.5; }
.card-clock__hand { stroke-linecap: round; transform-box: view-box; transform-origin: 56px 56px; }
.card-clock__hand--h { stroke: light-dark(#373a40, #f1f2f3); stroke-width: 4; transition: transform 0.9s cubic-bezier(0.3, 1.35, 0.5, 1); }
.card-clock__hand--m { stroke: light-dark(#373a40, #f1f2f3); stroke-width: 2.5; transition: transform 0.9s cubic-bezier(0.3, 1.35, 0.5, 1) 0.05s; }
.card-clock__hand--s { stroke: light-dark(#5f626a, #a7abb2); stroke-width: 1.2; transition: transform 0.25s cubic-bezier(0.3, 2, 0.5, 1); }
.card-clock__pin { fill: light-dark(#373a40, #f1f2f3); }
.card-clock__cities { display: grid; gap: 4px; flex: 1; }
.card-clock__city { height: 28px; padding: 0 10px; border: 0; border-radius: 8px; background: none; color: light-dark(#5f626a, #a7abb2); font: inherit; font-size: 12px; text-align: left; cursor: pointer; transition: background-color 0.2s, color 0.2s; }
.card-clock__city[aria-pressed='true'] { background: light-dark(#ebedf0, #232529); color: light-dark(#373a40, #f1f2f3); font-weight: 600; }
.card-clock__city:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 1px; }
.card-clock__read { display: flex; align-items: baseline; gap: 8px; margin: 10px 0 0; }
.card-clock__read b { font-size: 22px; font-weight: 700; color: light-dark(#373a40, #f1f2f3); font-variant-numeric: tabular-nums; }
.card-clock__read span { font-size: 11px; color: light-dark(#5f626a, #a7abb2); }
@media (prefers-reduced-motion: reduce) {
.card-clock__hand--h, .card-clock__hand--m, .card-clock__hand--s { transition: none; }
}function initCardClock(root) {
var CITIES = [
{ name: 'London', tz: 'Europe/London' },
{ name: 'Tokyo', tz: 'Asia/Tokyo' },
{ name: 'New York', tz: 'America/New_York' },
];
var hands = { h: root.querySelector('.card-clock__hand--h'), m: root.querySelector('.card-clock__hand--m'), s: root.querySelector('.card-clock__hand--s') };
var btns = root.querySelectorAll('.card-clock__city');
var digits = root.querySelector('.card-clock__read b');
var where = root.querySelector('.card-clock__read span');
var last = { h: 0, m: 0, s: 0 };
var city = 0;
var timer = 0;
function timeIn(tz, now) {
var parts = new Intl.DateTimeFormat('en-GB', { timeZone: tz, hour: 'numeric', minute: 'numeric', second: 'numeric', hourCycle: 'h23', timeZoneName: 'shortOffset' }).formatToParts(now);
function get(type) {
for (var i = 0; i < parts.length; i++) if (parts[i].type === type) return parts[i].value;
return '0';
}
return { h: +get('hour'), m: +get('minute'), s: +get('second'), zone: get('timeZoneName') };
}
// Keep an angle within half a turn of the last one, so a hand always takes the short way round.
function near(prev, a) {
return a + 360 * Math.round((prev - a) / 360);
}
function pad(n) {
return String(n).padStart(2, '0');
}
function render() {
var t = timeIn(CITIES[city].tz, new Date());
last.h = near(last.h, ((t.h % 12) + t.m / 60) * 30);
last.m = near(last.m, (t.m + t.s / 60) * 6);
last.s = near(last.s, t.s * 6);
['h', 'm', 's'].forEach(function (k) {
hands[k].style.transform = 'rotate(' + last[k] + 'deg)';
});
digits.textContent = pad(t.h) + ':' + pad(t.m);
where.textContent = CITIES[city].name + ' · ' + t.zone;
}
function tick() {
render();
timer = setTimeout(tick, 1000 - (Date.now() % 1000));
}
function onPick(e) {
city = Array.prototype.indexOf.call(btns, e.currentTarget);
btns.forEach(function (b, i) {
b.setAttribute('aria-pressed', String(i === city));
});
render();
}
btns.forEach(function (b) {
b.addEventListener('click', onPick);
});
tick();
return function () {
clearTimeout(timer);
btns.forEach(function (b) {
b.removeEventListener('click', onPick);
});
};
}
document.querySelectorAll('.card-clock').forEach(initCardClock);Goal: Create a "Clock" card.
Visual behavior: A world clock: pick a city and the hands swing round to its local time with a little overshoot, while the seconds hand keeps ticking.
Technique:
- Intl.DateTimeFormat gives each city's hour, minute, second and offset. The hands are SVG lines turned by a CSS rotate about the centre.
- Each new angle is kept within half a turn of the last one, so a hand always swings the short way round and never spins back through 12.
- The hour and minute hands swing on an overshooting curve; the seconds hand snaps each second with its own small bounce. A timer aligned to the second keeps it live.
Accessibility: The cities are buttons in a labelled group with aria-pressed, and the time is printed as text under the face, which is aria-hidden. Under prefers-reduced-motion the hands jump without swinging.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Clock" card.
Visual behavior: A world clock: pick a city and the hands swing round to its local time with a little overshoot, while the seconds hand keeps ticking.
Technique:
- Intl.DateTimeFormat gives each city's hour, minute, second and offset. The hands are SVG lines turned by a CSS rotate about the centre.
- Each new angle is kept within half a turn of the last one, so a hand always swings the short way round and never spins back through 12.
- The hour and minute hands swing on an overshooting curve; the seconds hand snaps each second with its own small bounce. A timer aligned to the second keeps it live.
Accessibility: The cities are buttons in a labelled group with aria-pressed, and the time is printed as text under the face, which is aria-hidden. Under prefers-reduced-motion the hands jump without swinging.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A world clock: pick a city and the hands swing round to its local time with a little overshoot, while the seconds hand keeps ticking.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, spring, state change, card
- Techniques
- svg, css transition, interval
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
The cities are buttons in a labelled group with aria-pressed, and the time is printed as text under the face, which is aria-hidden. Under prefers-reduced-motion the hands jump without swinging.