Displaying Times to Users in Their Local Time Zone

Developer
scheduling

Your server stores times in UTC. Your users are everywhere. The job of your frontend is to convert UTC to each user’s local time and format it readably. It’s more straightforward than it used to be, but there are a few ways to get it wrong.

Step 1: Get the user’s time zone

const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// → "America/New_York" or "Europe/London" etc.

This returns an IANA zone name from the browser’s locale settings. Reliable on modern browsers. Store it on the user’s profile so server-rendered pages can also use it — don’t rely solely on client-side conversion for anything indexed or cached.

Let users override it. Some people travel; some systems report the wrong zone.

Step 2: Convert and format in the browser

The Intl.DateTimeFormat API is the correct tool for this. It handles DST automatically and respects locale formatting conventions (12h vs 24h, date order, etc.):

const utcTimestamp = "2026-06-20T19:00:00Z"; // from your API
const date = new Date(utcTimestamp);

const formatter = new Intl.DateTimeFormat("en-US", {
  timeZone: "America/New_York",
  dateStyle: "full",
  timeStyle: "short",
});

formatter.format(date);
// → "Saturday, June 20, 2026 at 3:00 PM"

For a user in Tokyo:

new Intl.DateTimeFormat("ja-JP", {
  timeZone: "Asia/Tokyo",
  dateStyle: "full",
  timeStyle: "short",
}).format(date);
// → "2026年6月21日日曜日 4:00"  (next day in Tokyo)

Step 3: Include the time zone name in the display

Showing “3:00 PM” without a zone label leaves users unsure whether the conversion happened. Include the zone abbreviation or city:

new Intl.DateTimeFormat("en-US", {
  timeZone: "America/New_York",
  timeStyle: "long", // includes timezone abbreviation
}).format(date);
// → "3:00:00 PM EDT"

For scheduling tools, consider showing both the user’s local time and UTC:

3:00 PM EDT (19:00 UTC)

This lets users cross-check if they’re unsure the conversion is correct.

Common pitfalls

Manually subtracting hours:

// Bad: hardcodes UTC-4, breaks when DST changes
const localTime = new Date(utcTimestamp.getTime() - 4 * 60 * 60 * 1000);

Don’t do this. Use Intl.DateTimeFormat with an IANA zone name — it applies the correct offset for each specific date automatically.

toLocaleString() without a timeZone option:

// Unreliable: uses system locale, which varies by server/browser environment
date.toLocaleString();

Always pass timeZone explicitly.

“Today” and “tomorrow” based on UTC date:

2026-06-20T23:00:00Z is June 20 in New York and June 21 in Tokyo. If you label events as “today” or “tomorrow” based on the UTC date, users in some zones see the wrong label.

function isToday(utcTimestamp, userTimeZone) {
  const now = new Date();
  const formatter = new Intl.DateTimeFormat("en-CA", {
    // en-CA gives YYYY-MM-DD
    timeZone: userTimeZone,
    dateStyle: "short",
  });
  return formatter.format(new Date(utcTimestamp)) === formatter.format(now);
}

Sorting by display time: Always sort events by their UTC timestamp. Sorting by local time strings breaks across DST transitions and cross-zone comparisons.

Server-side rendering

If you render times on the server, you need the user’s IANA zone at render time. Store it in the session or user profile:

// Node.js — Intl is available in modern Node versions
new Intl.DateTimeFormat("en-US", {
  timeZone: user.timezone, // 'America/New_York' from profile
  dateStyle: "full",
  timeStyle: "short",
}).format(new Date(utcTimestamp));

Watch for hydration mismatches: if the server renders a time in UTC and the client re-renders it in local time, you’ll get a flash of wrong content. Either render times client-side only, or ensure the server has the correct zone at render time.

Buy me a coffe