X Xerobit

Timezone Conversion in JavaScript — Intl API, date-fns-tz, and Luxon

Convert dates between timezones in JavaScript using the Intl.DateTimeFormat API, date-fns-tz, and Luxon. Covers UTC offset vs named timezone, DST handling, user timezone...

Mian Ali Khalid · · 5 min read
Use the tool
Timestamp Converter
Convert Unix timestamps, epoch seconds/milliseconds, and ISO 8601 dates.
Open Timestamp Converter →

JavaScript’s Intl API supports IANA timezone names for display. For arithmetic across timezones, use date-fns-tz or Luxon which properly handle DST transitions.

Convert between timezone-aware timestamps with the Timestamp Converter.

UTC offset vs named timezone

// ❌ UTC offset: doesn't handle DST
const offset = new Date('2026-05-12T14:30:00-05:00');
// In November, UTC-5 is still UTC-5 — but CST actually becomes UTC-6 in summer

// ✅ IANA timezone name: correctly handles DST
// Use 'America/Chicago' instead of '-05:00'
// The system knows when DST switches occur

IANA timezone names look like America/New_York, Europe/London, Asia/Tokyo.

Display a date in any timezone (Intl API)

const date = new Date('2026-05-12T14:30:00Z');

function formatInTimezone(date, timezone, locale = 'en-US') {
  return new Intl.DateTimeFormat(locale, {
    timeZone: timezone,
    dateStyle: 'full',
    timeStyle: 'long',
  }).format(date);
}

formatInTimezone(date, 'America/New_York');
// "Tuesday, May 12, 2026 at 10:30:00 AM EDT"

formatInTimezone(date, 'Europe/London');
// "Tuesday, May 12, 2026 at 3:30:00 PM BST"

formatInTimezone(date, 'Asia/Tokyo');
// "Wednesday, May 13, 2026 at 11:30:00 PM JST"

// Format parts separately:
const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/Los_Angeles',
  year: 'numeric', month: '2-digit', day: '2-digit',
  hour: '2-digit', minute: '2-digit', timeZoneName: 'short',
});
formatter.format(date); // "05/12/2026, 07:30 AM PDT"

date-fns-tz: convert for arithmetic

npm install date-fns date-fns-tz
import { toZonedTime, fromZonedTime, formatInTimeZone } from 'date-fns-tz';
import { addDays, addHours } from 'date-fns';

const utcDate = new Date('2026-05-12T14:30:00Z');

// Convert to a zoned time (for display/arithmetic in that timezone):
const nyDate = toZonedTime(utcDate, 'America/New_York');
// nyDate represents 2026-05-12 10:30:00 in New York's clock time

// Format in timezone:
formatInTimeZone(utcDate, 'America/New_York', 'yyyy-MM-dd HH:mm zzz');
// "2026-05-12 10:30 EDT"

formatInTimeZone(utcDate, 'Asia/Kolkata', 'yyyy-MM-dd HH:mm zzz');
// "2026-05-12 20:00 IST"

// Add days in a timezone (important for DST!):
const nyMidnight = toZonedTime(new Date('2026-03-08T05:00:00Z'), 'America/New_York');
// 2026-03-08 is the night of US DST spring-forward
const nextDay = addDays(nyMidnight, 1); // Correctly handles the 23-hour day
const backToUtc = fromZonedTime(nextDay, 'America/New_York');

Luxon: most complete timezone support

import { DateTime } from 'luxon';

// UTC to timezone:
const utc = DateTime.fromISO('2026-05-12T14:30:00Z');
const ny = utc.setZone('America/New_York');
ny.toFormat('yyyy-MM-dd HH:mm zzz'); // "2026-05-12 10:30 EDT"

// Create in a specific timezone:
const tokyoNoon = DateTime.fromObject(
  { year: 2026, month: 5, day: 12, hour: 12, minute: 0 },
  { zone: 'Asia/Tokyo' }
);
tokyoNoon.toUTC().toISO(); // UTC equivalent

// List all timezones a user might pick:
// Luxon doesn't provide this directly — use Intl.supportedValuesOf
const timezones = Intl.supportedValuesOf('timeZone');
// ['Africa/Abidjan', 'Africa/Accra', ... 'UTC', ...]

Detect user’s timezone

// Get IANA name of user's local timezone:
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// "America/New_York", "Europe/Berlin", etc.

// Display current time in user's timezone:
function displayLocalTime(utcTimestamp) {
  const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
  return new Intl.DateTimeFormat('en-US', {
    timeZone: tz,
    dateStyle: 'medium',
    timeStyle: 'short',
  }).format(new Date(utcTimestamp));
}

Server-side: Node.js timezone handling

// Node.js uses the OS timezone by default
// Always work with UTC on the server and convert for display:

function utcToTimezone(utcDate, timezone) {
  const formatter = new Intl.DateTimeFormat('en-CA', {
    timeZone: timezone,
    year: 'numeric', month: '2-digit', day: '2-digit',
    hour: '2-digit', minute: '2-digit', second: '2-digit',
    hour12: false,
  });

  const parts = Object.fromEntries(
    formatter.formatToParts(utcDate).map(({ type, value }) => [type, value])
  );

  return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}`;
}

utcToTimezone(new Date('2026-05-12T14:30:00Z'), 'Asia/Tokyo');
// "2026-05-12T23:30:00"

Common pitfalls

// ❌ Don't use getTimezoneOffset() for named timezone conversion:
new Date().getTimezoneOffset(); // Returns LOCAL timezone offset, not arbitrary ones

// ❌ Don't construct dates with local time then convert:
new Date(2026, 4, 12, 14, 30); // Uses local timezone, not UTC

// ✅ Always start from UTC:
new Date('2026-05-12T14:30:00Z');
// or
new Date(Date.UTC(2026, 4, 12, 14, 30, 0));

Related posts

Related tool

Timestamp Converter

Convert Unix timestamps, epoch seconds/milliseconds, and ISO 8601 dates.

Written by Mian Ali Khalid. Part of the Dev Productivity pillar.