Skip to main content

date-fns-tz vs Luxon vs Spacetime: Timezone Handling in JavaScript (2026)

·PkgPulse Team

TL;DR

date-fns-tz extends date-fns with timezone support — converts between timezones, formats with timezone offsets, works with native Date objects, tree-shakeable. Luxon is the modern date/time library by the Moment.js team — immutable, built-in timezone support via Intl API, Duration/Interval types, the successor to Moment.js. Spacetime is the lightweight timezone library — timezone-aware date objects, DST handling, human-friendly API, small bundle. In 2026: date-fns-tz for date-fns users, Luxon for comprehensive date/time with timezones, Spacetime for lightweight timezone operations.

Key Takeaways

  • date-fns-tz: ~5M weekly downloads — date-fns extension, native Date, tree-shakeable
  • Luxon: ~10M weekly downloads — Moment.js successor, immutable, Intl-based timezones
  • Spacetime: ~500K weekly downloads — lightweight, timezone-first, human-friendly API
  • date-fns-tz extends an existing library; Luxon and Spacetime are standalone
  • Luxon has the most comprehensive API (Duration, Interval, timezone-aware)
  • Spacetime has the smallest learning curve for timezone work

date-fns-tz

date-fns-tz — timezone support for date-fns:

Convert between timezones

import { formatInTimeZone, toZonedTime, fromZonedTime } from "date-fns-tz"

const utcDate = new Date("2026-03-09T12:00:00Z")

// Format in a specific timezone:
formatInTimeZone(utcDate, "America/New_York", "yyyy-MM-dd HH:mm:ss zzz")
// → "2026-03-09 07:00:00 EST"

formatInTimeZone(utcDate, "Asia/Tokyo", "yyyy-MM-dd HH:mm:ss zzz")
// → "2026-03-09 21:00:00 JST"

formatInTimeZone(utcDate, "Europe/London", "yyyy-MM-dd HH:mm:ss zzz")
// → "2026-03-09 12:00:00 GMT"

// Convert UTC to zoned time (for display):
const nyTime = toZonedTime(utcDate, "America/New_York")
// → Date representing 7:00 AM (local equivalent)

// Convert zoned time back to UTC (for storage):
const backToUtc = fromZonedTime(new Date(2026, 2, 9, 7, 0), "America/New_York")
// → Date representing 12:00 PM UTC

Format with timezone info

import { formatInTimeZone } from "date-fns-tz"

const date = new Date("2026-03-09T12:00:00Z")

// Various format tokens:
formatInTimeZone(date, "America/New_York", "h:mm a z")
// → "7:00 AM EST"

formatInTimeZone(date, "America/New_York", "HH:mm:ss OOOO")
// → "07:00:00 GMT-05:00"

formatInTimeZone(date, "America/New_York", "PPP 'at' p zzz")
// → "March 9th, 2026 at 7:00 AM Eastern Standard Time"

With date-fns functions

import { addHours, isBefore, differenceInHours } from "date-fns"
import { formatInTimeZone, toZonedTime } from "date-fns-tz"

const meeting = new Date("2026-03-09T15:00:00Z")

// Standard date-fns operations work:
const reminder = addHours(meeting, -1)
const diff = differenceInHours(meeting, new Date())

// Then format in any timezone:
formatInTimeZone(meeting, "America/Los_Angeles", "h:mm a z")
// → "7:00 AM PST"

formatInTimeZone(meeting, "Europe/Berlin", "H:mm z")
// → "16:00 CET"

Luxon

Luxon — modern date/time library:

Timezone support

import { DateTime } from "luxon"

// Create in a timezone:
const ny = DateTime.now().setZone("America/New_York")
console.log(ny.toFormat("yyyy-MM-dd HH:mm:ss ZZZZ"))
// → "2026-03-09 07:00:00 Eastern Standard Time"

// Create from specific timezone:
const tokyo = DateTime.fromObject(
  { year: 2026, month: 3, day: 9, hour: 21 },
  { zone: "Asia/Tokyo" }
)
console.log(tokyo.toISO())
// → "2026-03-09T21:00:00.000+09:00"

// Convert between timezones:
const londonTime = tokyo.setZone("Europe/London")
console.log(londonTime.toFormat("HH:mm z"))
// → "12:00 GMT"

// UTC:
const utc = DateTime.utc(2026, 3, 9, 12, 0)
console.log(utc.setZone("America/Chicago").toFormat("h:mm a z"))
// → "6:00 AM CST"

Timezone comparison

import { DateTime } from "luxon"

// What time is it in multiple cities?
const now = DateTime.now()
const zones = ["America/New_York", "Europe/London", "Asia/Tokyo", "Australia/Sydney"]

zones.forEach((zone) => {
  const time = now.setZone(zone)
  console.log(`${zone}: ${time.toFormat("HH:mm (ZZZZ)")}`)
})
// → America/New_York: 07:00 (Eastern Standard Time)
// → Europe/London: 12:00 (Greenwich Mean Time)
// → Asia/Tokyo: 21:00 (Japan Standard Time)
// → Australia/Sydney: 23:00 (Australian Eastern Daylight Time)

// Timezone offset:
const ny = DateTime.now().setZone("America/New_York")
console.log(ny.offset)       // → -300 (minutes from UTC)
console.log(ny.offsetNameShort) // → "EST"
console.log(ny.offsetNameLong)  // → "Eastern Standard Time"
console.log(ny.zoneName)        // → "America/New_York"

Duration and Interval

import { DateTime, Duration, Interval } from "luxon"

// Duration:
const duration = Duration.fromObject({ hours: 5, minutes: 30 })
console.log(duration.toFormat("h 'hours' m 'minutes'"))
// → "5 hours 30 minutes"

const meeting = DateTime.now().setZone("America/New_York")
const end = meeting.plus(duration)
console.log(end.toFormat("h:mm a z"))

// Interval:
const workday = Interval.fromDateTimes(
  DateTime.fromObject({ hour: 9 }, { zone: "America/New_York" }),
  DateTime.fromObject({ hour: 17 }, { zone: "America/New_York" })
)
console.log(workday.length("hours")) // → 8
console.log(workday.contains(DateTime.now().setZone("America/New_York")))

DST handling

import { DateTime } from "luxon"

// Luxon handles DST transitions automatically:
const beforeDST = DateTime.fromObject(
  { year: 2026, month: 3, day: 8, hour: 1, minute: 30 },
  { zone: "America/New_York" }
)
console.log(beforeDST.toFormat("h:mm a z")) // → "1:30 AM EST"

const afterDST = beforeDST.plus({ hours: 1 })
console.log(afterDST.toFormat("h:mm a z"))  // → "3:30 AM EDT"
// (skips 2:00 AM — spring forward)

// Check if in DST:
console.log(afterDST.isInDST) // → true

Spacetime

Spacetime — lightweight timezone library:

Basic usage

import spacetime from "spacetime"

// Current time in a timezone:
const ny = spacetime.now("America/New_York")
console.log(ny.format("nice"))
// → "Mar 9th, 7:00am"

const tokyo = spacetime.now("Asia/Tokyo")
console.log(tokyo.format("nice"))
// → "Mar 9th, 9:00pm"

// Create specific date in timezone:
const date = spacetime("2026-03-09", "Europe/London")
console.log(date.format("iso"))
// → "2026-03-09T00:00:00.000+00:00"

Timezone conversion

import spacetime from "spacetime"

const meeting = spacetime("2026-03-09 3:00pm", "America/New_York")

// Convert to other timezones:
console.log(meeting.goto("Europe/London").format("time"))
// → "8:00pm"

console.log(meeting.goto("Asia/Tokyo").format("time"))
// → "5:00am" (next day)

console.log(meeting.goto("America/Los_Angeles").format("time"))
// → "12:00pm"

// Chain conversions:
const result = spacetime.now("America/New_York")
  .goto("UTC")
  .format("iso")

Human-friendly API

import spacetime from "spacetime"

const s = spacetime.now("America/New_York")

// Getters:
s.hour()       // 7
s.minute()     // 0
s.dayName()    // "monday"
s.monthName()  // "march"
s.timezone().name  // "America/New_York"

// Setters (immutable):
const noon = s.hour(12).minute(0)
const nextWeek = s.add(1, "week")
const startOfDay = s.startOf("day")
const endOfMonth = s.endOf("month")

// Format:
s.format("nice-short")    // "Mar 9, 7:00am"
s.format("nice-year")     // "Mar 9th, 2026"
s.format("{month} {date-ordinal}, {year}")  // "March 9th, 2026"
s.format("iso")           // "2026-03-09T07:00:00.000-05:00"

DST handling

import spacetime from "spacetime"

const s = spacetime("2026-03-08", "America/New_York")

// Check DST:
console.log(s.isDST())         // false (before spring forward)
console.log(s.hasDST())        // true (this timezone uses DST)
console.log(s.offset())        // -300 (minutes, EST = UTC-5)

// After spring forward:
const after = s.add(1, "day")
console.log(after.isDST())     // true
console.log(after.offset())    // -240 (minutes, EDT = UTC-4)

// When does DST change?
const changes = s.timezone().change
// → { start: "March 8", back: "November 1" }

Feature Comparison

Featuredate-fns-tzLuxonSpacetime
Typedate-fns extensionStandalone libraryStandalone library
Timezone support✅ (built-in)✅ (core feature)
IANA timezones
DST handling
Immutable✅ (native Date)
Duration/IntervalVia date-fns
Formatting
Tree-shakeable
Bundle size~5KB (+ date-fns)~70KB~40KB
TypeScript
Intl API based
Relative timeVia date-fns
Weekly downloads~5M~10M~500K

When to Use Each

Use date-fns-tz if:

  • Already using date-fns for date operations
  • Want tree-shakeable timezone functions
  • Prefer working with native Date objects
  • Need only timezone conversion and formatting

Use Luxon if:

  • Need a comprehensive date/time library with timezones
  • Want Duration, Interval, and timezone support built-in
  • Migrating from Moment.js (same team, spiritual successor)
  • Need the richest API for date/time operations

Use Spacetime if:

  • Want the most human-friendly timezone API
  • Need a lightweight library focused on timezones
  • Building timezone conversion tools or world clocks
  • Prefer natural language methods (dayName, monthName)

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on date-fns-tz v3.x, Luxon v3.x, and Spacetime v7.x.

Compare date/time libraries and developer tooling on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.