Alex Vipond
SHIFT + D

Solution tier list: Date manipulation

S
@internationalized/date + Intl.DateTimeFormat
A
Temporal + Intl.DateTimeFormat
B
date-fns
C
D
Date + Intl.DateTimeFormat
E
Format on the server
F
Apply timezone offsets on the server

A while back, I wrote a tier list for styling solutions in legacy projects. I think another one is needed for date manipulation.

Let's manipulate today's date!

S tier

@internationalized/date and Intl.DateTimeFormat are hands-down the best way to manipulate and format dates:

// `@internationalized/date` is managed by the React Aria team
import { fromAbsolute } from '@internationalized/date'

const {
  timestamp, // Milliseconds since January 1, 1970, 00:00:00 UTC
  userPreferences: {
    timezone, // e.g. "America/Chicago"
    locale, // e.g. "en-US"
  },
} = await (await fetch('/api/some-endpoint')).json()

// ZonedDateTime instances are timezone-aware:
const zonedDateTime = fromAbsolute(
  timestamp,
  userPreferredTimezone
)

// Easily add or subtract time:
zonedDateTime.add({ years: 1, months: 1, days: 1 })
zonedDateTime.subtract({ years: 1, months: 1, days: 1 })

// Convert back to a Date instance so you can
// format it with Intl.DateTimeFormat:
const date = zonedDateTime.toDate()
const formatted = new Intl.DateTimeFormat(
  locale,
  {
    timeZone: timezone,
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    hour: 'numeric',
    minute: 'numeric',
    timeZoneName: 'short',
  }
).format(date) // June 25, 2025, 4:20 PM CST

A tier

When the React Aria team wrote @internationalized/date, they took inspiration from Temporal, an upcoming JS standard.

Temporal will eventually be S-tier, but I have to rank it lower until it has good browser support.

const {
  timestamp, // Milliseconds since January 1, 1970, 00:00:00 UTC
  userPreferences: {
    timezone, // e.g. "America/Chicago"
    locale, // e.g. "en-US"
  },
} = await (await fetch('/api/some-endpoint')).json()

// Temporal.ZonedDateTime instances are timezone-aware:
const zonedDateTime = new Temporal.ZonedDateTime(
  timestamp,
  userPreferredTimezone
)

// Easily add or subtract time:
zonedDateTime.add(new Temporal.Duration(1, 1, 1))
zonedDateTime.subtract(new Temporal.Duration(1, 1, 1))

// Convert to a Temporal.Instant so you can
// format it with Intl.DateTimeFormat:
const instant = zonedDateTime.toInstant()
const formatted = new Intl.DateTimeFormat(
  locale,
  {
    timeZone: timezone,
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    hour: 'numeric',
    minute: 'numeric',
    timeZoneName: 'short',
  }
).format(instant) // June 25, 2025, 4:20 PM CST

B tier

date-fns has always been great, but it got even better in late 2024 when v4 added timezone support.

The new TZDate class stores timezone information so that date-fns' date math functions can take timezones into account. It's compatible with Intl.DateTimeFormat, too.

I love date-fns' functional programming style, and I'm tempted to rank it S-tier. The only thing that holds me back: Temporal will probably make it obsolete, and it's hard to justify learning date-fns' unique APIs just to watch them go obsolete in the near future.

That said, I wouldn't mind a functional-programming-style wrapper around Temporal 🤔

import { TZDate } from '@date-fns/tz'
import { add, sub } from 'date-fns'

const {
  timestamp, // Milliseconds since January 1, 1970, 00:00:00 UTC
  userPreferences: {
    timezone, // e.g. "America/Chicago"
    locale, // e.g. "en-US"
  },
} = await (await fetch('/api/some-endpoint')).json()

// date-fns v4 is timezone-aware:
const tzDate = new TZDate(timestamp, timezone)

// Add or subtract time:
const added = add(tzDate, { years: 1, months: 1, days: 1 })
const subtracted = sub(tzDate, { years: 1, months: 1, days: 1 })

// TZDate extends the native Date object, which means you can
// format directly with Intl.DateTimeFormat.
const formatted = new Intl.DateTimeFormat(
  locale,
  {
    timeZone: timezone,
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    hour: 'numeric',
    minute: 'numeric',
    timeZoneName: 'short',
  }
).format(tzDate) // June 25, 2025, 4:20 PM CST

C tier

Skipping over C tier, because the remaining solutions kind of suck!

D tier

The built-in Date object works really well with Intl.DateTimeFormat for locale-aware, timezone-aware formatting, but date math is brutal.

The easiest solution is to do arithmetic on the underlying UTC timestamp, but this leaves you wide open to bugs with daylight saving time, leap years, and other calendar system quirks.

const {
  timestamp, // Milliseconds since January 1, 1970, 00:00:00 UTC
  userPreferences: {
    timezone, // e.g. "America/Chicago"
    locale, // e.g. "en-US"
  },
} = await (await fetch('/api/some-endpoint')).json()

// Dates are timezone-unaware, locked in as UTC
const date = new Date(timestamp)

// Add or subtract time. It's always custom arithmetic,
// and zero awareness of different calendar systems,
// daylight saving time, etc.
const added = new Date(
  date.valueOf()
  + 1000 * 60 * 60 * 24 * 365 // Add one year
  + 1000 * 60 * 60 * 24 * 30 // Add one month
  + 1000 * 60 * 60 * 24 // Add one day
)
const subtracted = new Date(
  date.valueOf()
  - 1000 * 60 * 60 * 24 * 365 // Subtract one year
  - 1000 * 60 * 60 * 24 * 30 // Subtract one month
  - 1000 * 60 * 60 * 24 // Subtract one day
)

// Format with Intl.DateTimeFormat, which is timezone-aware:
const formatted = new Intl.DateTimeFormat(
  locale,
  {
    timeZone: timezone,
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    hour: 'numeric',
    minute: 'numeric',
    timeZoneName: 'short',
  }
).format(date) // June 25, 2025, 4:20 PM CST

E tier

F tier

Related

createDateFormat useDateFormatter

ON THIS PAGE

Solution tier list: Date manipulationS tierA tierB tierC tierD tierE tierF tierRelated