Local Time
The time on the visitor's own clock, or in another place's time zone, following the clock.
$npx shadcn@latest add https://framebrick.co/r/local-time.jsonFrom the Marai Neesu template, see it in context.
- Your time
- Dubai
- Tokyo
import { LocalTime } from "@/components/ui/local-time";
const places = [
{ name: "Your time" },
{ name: "Dubai", timeZone: "Asia/Dubai" },
{ name: "Tokyo", timeZone: "Asia/Tokyo" },
];
export default function LocalTimeDemo() {
return (
<ul className="w-full max-w-64 divide-y rounded-lg border text-sm">
{places.map((place) => (
<li key={place.name} className="flex items-center justify-between px-4 py-3">
<span className="text-muted-foreground">{place.name}</span>
<LocalTime timeZone={place.timeZone} fallback="--:-- --" className="font-medium" />
</li>
))}
</ul>
);
}Examples
With seconds
A 24-hour clock that ticks every second, in tabular figures.
Your time, to the second
import { LocalTime } from "@/components/ui/local-time";
export default function LocalTimeSecondsDemo() {
return (
<div className="text-center">
<LocalTime seconds hour12={false} fallback="--:--:--" className="text-4xl font-semibold tracking-tight" />
<p className="mt-2 text-sm text-muted-foreground">Your time, to the second</p>
</div>
);
}<LocalTime seconds hour12={false} className="text-4xl font-semibold" />Installation
$ npx shadcn@latest add https://framebrick.co/r/local-time.json1. Install the dependencies.
npm install motion clsx tailwind-merge2. Add
cn()tolib/utils.ts(skip if shadcn already created it).import { clsx, type ClassValue } from "clsx"; import { extendTailwindMerge } from "tailwind-merge"; /* tailwind-merge must know the site's type scale (globals.css) is a font size, not a text color — otherwise cn("text-h6 text-muted-foreground") drops text-h6. */ const twMerge = extendTailwindMerge({ extend: { classGroups: { "font-size": [{ text: ["display", "h1", "h2", "h3", "h4", "h5", "h6"] }] } }, }); export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }3. Copy the component into
components/ui/local-time.tsx."use client"; import * as React from "react"; import { cn } from "@/lib/utils"; /* * Local Time — the time on a visitor's own clock, or in another place's time zone ("7:41 PM"), following the clock. * It is Framer's "Time & Date" code component, which three of our templates carry (the Ciketo header, the Lookis header and * footer, the Marai Neesu footer), as a `<time>` element. * * It ticks on the minute, or on the second with `seconds`, aligned to the real boundary (not a timer that drifts), and * draws again when the tab comes back to the front. Only a string that changed is drawn, and figures are tabular, so the * text does not shift as the digits change. * * The server cannot know the visitor's moment, so it draws `fallback` (nothing by default) and the browser fills the time * in right after hydration, which React does not count as a mismatch: the time is read through useSyncExternalStore with * a server snapshot of null. Give `fallback` a placeholder the width of the time ("--:-- --") when the row must not move. * `timeZone` takes any IANA zone ("Asia/Dubai"); the visitor's own is the default. `options` is Intl.DateTimeFormat's, for a * date or a long form. */ export type LocalTimeProps = Omit<React.ComponentProps<"time">, "children" | "dateTime"> & { /** An IANA zone such as "Asia/Dubai"; the visitor's own zone when left out. */ timeZone?: string; /** BCP 47 locale. "en-US" draws "7:41 PM". */ locale?: string; /** Show seconds; the clock then ticks every second instead of every minute. */ seconds?: boolean; /** Force a 12 or a 24 hour clock; the locale decides when left out. */ hour12?: boolean; /** Any Intl.DateTimeFormat options, in place of the hour and minute (`{ dateStyle: "medium" }` draws a date). */ options?: Intl.DateTimeFormatOptions; /** What the server draws, and the browser until it has the time. Nothing by default. */ fallback?: React.ReactNode; }; export function LocalTime({ timeZone, locale = "en-US", seconds = false, hour12, options, fallback, className, ...props }: LocalTimeProps) { /* A key for the options that matter, so a new object with the same content does not rebuild the formatter. */ const optionsKey = JSON.stringify(options ?? null); const formatter = React.useMemo( () => new Intl.DateTimeFormat(locale, { ...(options ?? { hour: "numeric", minute: "2-digit", ...(seconds ? { second: "2-digit" } : {}) }), ...(hour12 === undefined ? {} : { hour12 }), ...(timeZone ? { timeZone } : {}), }), // eslint-disable-next-line react-hooks/exhaustive-deps [locale, seconds, hour12, timeZone, optionsKey], ); const ticksEverySecond = formatter.resolvedOptions().second !== undefined || (options?.fractionalSecondDigits ?? 0) > 0; const period = ticksEverySecond ? 1000 : 60_000; const subscribe = React.useCallback( (notify: () => void) => { let timer = 0; const tick = () => { notify(); timer = window.setTimeout(tick, period - (Date.now() % period)); }; timer = window.setTimeout(tick, period - (Date.now() % period)); const visible = () => document.visibilityState === "visible" && notify(); document.addEventListener("visibilitychange", visible); return () => { window.clearTimeout(timer); document.removeEventListener("visibilitychange", visible); }; }, [period], ); const text = React.useSyncExternalStore( subscribe, () => formatter.format(new Date()), () => null, ); return ( <time {...props} dateTime={text === null ? undefined : new Date().toISOString()} className={cn("tabular-nums", className)}> {text ?? fallback} </time> ); }
Usage
import { LocalTime } from "@/components/ui/local-time";
<LocalTime />
<LocalTime timeZone="Asia/Dubai" fallback="--:-- --" />
<LocalTime seconds hour12={false} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| timeZone | string | — | An IANA zone such as "Asia/Dubai"; the visitor's own zone when left out. |
| locale | string | "en-US" | BCP 47 locale. "en-US" draws "7:41 PM". |
| seconds | boolean | false | Show seconds; the clock then ticks every second instead of every minute. |
| hour12 | boolean | — | Force a 12 or a 24 hour clock; the locale decides when left out. |
| options | Intl.DateTimeFormatOptions | — | Any Intl.DateTimeFormat options, in place of the hour and minute ({ dateStyle: "medium" } draws a date). |
| fallback | ReactNode | — | What the server draws, and the browser until it has the time. Nothing by default. |
| …props | time props | — | Passed to the <time> element (className, style, aria-*). |
- Three of our templates carry it as Framer's Time & Date code component: the Ciketo header (24-hour, over the date), the Lookis header and footer (the visitor's time) and the Marai Neesu footer (a place's time).
- The server cannot know the visitor's moment, so it draws the fallback and the browser fills the time in right after hydration. React does not count that as a mismatch: the time is read through useSyncExternalStore with a server snapshot of null. Give fallback a placeholder the width of the time when the row must not move.
- It ticks on the real boundary of the minute (the second with seconds), not on a timer that drifts, and draws again when the tab comes back to the front. Figures are tabular, so the text does not shift as the digits change.
- It is a <time> element with a dateTime, not a live region: a clock that announced itself would speak every minute.
AI prompt
Paste into v0, Lovable, Bolt or Claude to rebuild this component in your own project.
Create a React + Tailwind CSS component called LocalTime — a client component that renders a <time> element with the current time. No animation. - Props: timeZone (IANA zone; the visitor's own when missing), locale (default "en-US"), seconds (boolean), hour12 (boolean), options (Intl.DateTimeFormatOptions, replacing the default hour numeric + minute 2-digit), fallback (ReactNode), className and the <time> attributes. - Read the time with React.useSyncExternalStore: the snapshot is Intl.DateTimeFormat(locale, options).format(new Date()) as a string, the server snapshot is null, so the server draws the fallback and the browser fills the time in after hydration without a mismatch. - subscribe: one setTimeout chain aligned to the real boundary (60 000 ms, or 1000 with seconds: delay = period - (Date.now() % period)), and refresh when the tab becomes visible (visibilitychange). Only a changed string re-renders. - Tabular figures (tabular-nums). dateTime attribute = the ISO time once it is known. Not a live region.











