Time Display

A timezone-aware clock for your site, with a stable layout as the time updates.

A small timezone-aware clock built for navigation bars, profile pages, and anywhere you want to show a local time.

It starts with an invisible placeholder so server rendering and client hydration always agree. Once mounted, it updates every second. Tabular numbers keep each digit the same width, so the surrounding layout stays still as the seconds change.

Pass any IANA timezone string. Invalid values render as --:--:-- instead of throwing.

Install dependencies

npm install lucide-react

Source code

TimeDisplay.tsx
"use client";

import { cn } from "@/lib/utils";
import { Clock } from "lucide-react";
import { useEffect, useState } from "react";

export interface TimeDisplayProps {
  timeZone?: string;
  showIcon?: boolean;
  className?: string;
}

function formatTime(timeZone: string) {
  const parts = new Intl.DateTimeFormat("en-GB", {
    timeZone,
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
    hourCycle: "h23",
  }).formatToParts(new Date());

  const get = (type: Intl.DateTimeFormatPartTypes) =>
    parts.find((p) => p.type === type)?.value ?? "00";

  return `${get("hour")}:${get("minute")}:${get("second")}`;
}

function isValidTimeZone(timeZone: string) {
  try {
    Intl.DateTimeFormat("en-US", { timeZone });
    return true;
  } catch {
    return false;
  }
}

export default function TimeDisplay({
  timeZone = "Asia/Dhaka",
  showIcon = true,
  className,
}: TimeDisplayProps) {
  const [time, setTime] = useState("");
  const [invalid, setInvalid] = useState(false);

  useEffect(() => {
    if (!timeZone.trim() || !isValidTimeZone(timeZone)) {
      setInvalid(true);
      setTime("--:--:--");
      return;
    }

    setInvalid(false);

    const tick = () => {
      try {
        setTime(formatTime(timeZone));
        setInvalid(false);
      } catch {
        setInvalid(true);
        setTime("--:--:--");
      }
    };

    tick();
    const id = window.setInterval(tick, 1000);
    return () => window.clearInterval(id);
  }, [timeZone]);

  // SSR / first paint — invisible stable skeleton
  if (!time) {
    return (
      <span
        className={cn(
          "inline-flex items-center gap-[0.35em] opacity-0",
          className
        )}
        aria-hidden
      >
        {showIcon ? (
          <Clock className="size-[0.85em] shrink-0" strokeWidth={2.25} />
        ) : null}
        <span className="tabular-nums">00:00:00</span>
      </span>
    );
  }

  return (
    <span
      className={cn(
        "inline-flex items-center gap-[0.35em] leading-none",
        invalid && "text-muted-foreground",
        className
      )}
    >
      {showIcon ? (
        <Clock
          className="size-[0.85em] shrink-0 opacity-70"
          strokeWidth={2.25}
          aria-hidden
        />
      ) : null}
      <time
        className="tabular-nums tracking-tight"
        aria-label={
          invalid ? "Invalid timezone" : `Current time in ${timeZone}`
        }
      >
        {time}
      </time>
    </span>
  );
}