date-fns-tz vs Luxon vs Spacetime: Timezone Handling in JavaScript (2026)
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
| Feature | date-fns-tz | Luxon | Spacetime |
|---|---|---|---|
| Type | date-fns extension | Standalone library | Standalone library |
| Timezone support | ✅ | ✅ (built-in) | ✅ (core feature) |
| IANA timezones | ✅ | ✅ | ✅ |
| DST handling | ✅ | ✅ | ✅ |
| Immutable | ✅ (native Date) | ✅ | ✅ |
| Duration/Interval | Via date-fns | ✅ | ❌ |
| Formatting | ✅ | ✅ | ✅ |
| Tree-shakeable | ✅ | ❌ | ❌ |
| Bundle size | ~5KB (+ date-fns) | ~70KB | ~40KB |
| TypeScript | ✅ | ✅ | ✅ |
| Intl API based | ✅ | ✅ | ✅ |
| Relative time | Via 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 →