I wanted a big, clean clock I could keep on a second screen during meetings (and occasionally full-screen on a TV). Most clock sites are either too busy or not quite the style I wanted, so I built my own:
- Live site: https://www.uni-datetime.site/
This post is a quick walkthrough of the technical approach and a few lessons learned while getting the flip animation to feel "right".
What I built
Uni Datetime is a minimal flip clock web page:
- Large flip digits (HH:MM:SS)
- Date above time
- Light/Dark mode
- 12/24-hour toggle
- Fullscreen toggle (ESC exits fullscreen)
Time and timezone are derived from the browser/system timezone, so if you change your browser timezone (or simulate one in DevTools), the displayed time changes accordingly.
Stack
I kept it simple and static:
- Astro for a fast, static build
- React for the interactive clock "island"
- Luxon for date/time formatting
Astro gives me a tiny HTML shell and bundles a React component for the clock UI and controls.
The flip digit: two halves + two animated layers
The flip effect comes from rendering four layers:
-
upper(static top half) -
lower(static bottom half) -
flip-upper(animated top half that flips down) -
flip-lower(animated bottom half that flips in)
Visually, the key is: the flip uses the old value until the midpoint, then reveals the new value.
In React, each digit keeps current and next values, plus a play flag for the CSS animation:
const FLIP_MS = 600;
function FlipDigit({ value }: { value: string }) {
const [play, setPlay] = useState(false);
const [current, setCurrent] = useState(value);
const [next, setNext] = useState(value);
useLayoutEffect(() => {
if (value === current) {
setNext(value);
setPlay(false);
return;
}
// Start flip from current -> value
setNext(value);
setPlay(true);
const t = window.setTimeout(() => {
setCurrent(value);
setPlay(false);
}, FLIP_MS);
return () => window.clearTimeout(t);
}, [value, current]);
return (
<div className={`flip-digit${play ? " play" : ""}`}>
<div className="upper"><span>{play ? next : current}</span></div>
<div className="lower"><span>{current}</span></div>
<div className="flip-upper"><span>{current}</span></div>
<div className="flip-lower"><span>{next}</span></div>
</div>
);
}
And the CSS animation is split into two phases (top flips first, then bottom):
.flip-digit { perspective: 1000px; }
.flip-upper { transform-origin: bottom; }
.flip-lower { transform-origin: top; transform: rotateX(90deg); }
.flip-digit.play .flip-upper { animation: flipUpper 0.3s ease-in forwards; }
.flip-digit.play .flip-lower { animation: flipLower 0.3s ease-out 0.3s forwards; }
@keyframes flipUpper { to { transform: rotateX(-90deg); } }
@keyframes flipLower { to { transform: rotateX(0deg); } }
Keeping it smooth (and avoiding "new value flips first")
The most common bug I hit: the digit briefly shows the new number on the top half before the flip starts, which ruins the illusion.
The fix was to:
- Treat the currently-rendered value (
current) as authoritative for the static lower half - Only update
currentafter the flip finishes - Use a layout effect (
useLayoutEffect) so DOM updates and animation class toggles happen without visible flicker
Preferences: theme + 12/24 format
Preferences are simple:
- Store
themeandformatinlocalStorage - Apply theme via
document.documentElement.dataset.theme - Format time via Luxon:
-
HHmmssfor 24-hour -
hhmmss+afor 12-hour + AM/PM
-
I also used tabular numbers (font-variant-numeric: tabular-nums) to prevent digit width jitter.
Fullscreen toggle
Fullscreen is handled with the standard Fullscreen API (requestFullscreen / exitFullscreen). ESC exits fullscreen by browser default; the app just listens for fullscreenchange to keep the button label in sync.
Accessibility and motion
Flip animations look great, but not everyone wants them:
- If
prefers-reduced-motion: reduceis set, the animated layers are hidden and the clock becomes a static digit update.
Small SEO baseline (for a single-page tool)
Even for a simple tool, it's worth adding:
- A good
<title>and meta description - A canonical URL
- A
sitemap.xml
Next on my list: OpenGraph/Twitter cards and JSON-LD (structured data) so links look better when shared.
Try it / feedback
If you have thoughts on the animation timing, typography, or anything you'd like to see added, I'd love feedback:
Top comments (0)