$ snippet timezone-dst-safe-dates

Convert dates across time zones (DST-safe)

Format any instant in a named IANA time zone, and get the current UTC offset for that zone — daylight saving handled by the browser.

JavaScript
/** Format an instant in a given IANA time zone, e.g. "America/New_York". */
function formatInZone(date, timeZone, locale = "en-US") {
  return new Intl.DateTimeFormat(locale, {
    timeZone, year: "numeric", month: "2-digit", day: "2-digit",
    hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
    timeZoneName: "short",
  }).format(date);
}

/** UTC offset (in minutes) of a zone at a given instant. Accounts for DST. */
function zoneOffsetMinutes(date, timeZone) {
  const parts = new Intl.DateTimeFormat("en-US", {
    timeZone, hourCycle: "h23", year: "numeric", month: "2-digit", day: "2-digit",
    hour: "2-digit", minute: "2-digit", second: "2-digit",
  }).formatToParts(date).reduce((o, p) => (o[p.type] = p.value, o), {});
  const asUTC = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
  return Math.round((asUTC - date.getTime()) / 60000);
}

/** Build a Date from wall-clock components in a zone ("9:00 in Tokyo"). */
function dateFromZone(y, m, d, h, min, timeZone) {
  const guess = new Date(Date.UTC(y, m - 1, d, h, min));
  const offset = zoneOffsetMinutes(guess, timeZone);
  const result = new Date(guess.getTime() - offset * 60000);
  // second pass in case the guess straddled a DST switch
  const offset2 = zoneOffsetMinutes(result, timeZone);
  return offset2 === offset ? result : new Date(guess.getTime() - offset2 * 60000);
}

How it works

A JavaScript Date is just a UTC timestamp; the time zone only matters when you format it or build it from parts. Intl.DateTimeFormat knows every zone's DST rules, so you never hard-code offsets like -5. The third helper solves the common "user entered 9:00 in their zone, store it as UTC" problem, including the day the clocks change.

Gotchas

Try it
Result
Ad slot