Temporal API: The Modern Replacement for JavaScript Date
March 14, 2026
On March 11, 2026, Temporal reached TC39 Stage 4 and became part of the ES2026 specification. It is now natively shipped in Chrome 144+, Firefox 139+, and Edge 144+.
Temporal is JavaScript's new datetime API — the language-level answer to thirty years of Date design problems. If you've been reaching for dayjs, date-fns, or moment.js to fill gaps that Date leaves open, Temporal is what makes those libraries unnecessary.
Temporal's Types
Temporal lives as a top-level namespace (like Math or Intl) with a family of distinct types, each purpose-built for a specific use case:
Type
When to use
Temporal.PlainDate
Birthdays, holidays — date only, no time, no timezone
An exact point in time, for storage or cross-timezone comparison
Temporal.Duration
The span between two points in time
Temporal.PlainTime
Time-of-day without a date
All Temporal objects are immutable — operations always return new objects, originals are never modified. When in doubt, start with ZonedDateTime.
Here are a few concrete examples you can run directly in the browser:
The examples on this page are editable and include the @js-temporal/polyfill, so they run in any browser. If your browser natively supports Temporal (Chrome 144+, Firefox 139+, Edge 144+), the polyfill is skipped and the native implementation is used instead.
Example 1: Duration Arithmetic
With Date, calculating "how long between two times" means subtracting milliseconds, then manually dividing by 86400000 for days, taking the remainder, dividing by 3600000 for hours... Temporal gives you a Duration object with days, hours, and minutes already separated, plus .total('hours') to convert to a single unit:
Try it: Date version — duration arithmetic
JS
const startDate =newDate('2024-03-10T09:00:00');const endDate =newDate('2024-03-11T17:30:00');const diffMs = endDate - startDate;const days =Math.floor(diffMs /(1000*60*60*24));const hours =Math.floor((diffMs %(1000*60*60*24))/(1000*60*60));const mins =Math.floor((diffMs %(1000*60*60))/(1000*60));console.log('Result:', days +'d', hours +'h', mins +'m');
Try it: Temporal version — duration arithmetic
TemporalJS
const start =Temporal.PlainDateTime.from('2024-03-10T09:00:00');const end =Temporal.PlainDateTime.from('2024-03-11T17:30:00');const duration = start.until(end,{largestUnit:'day'});console.log('ISO 8601:', duration.toString());console.log('Days:', duration.days,'Hours:', duration.hours,'Minutes:', duration.minutes);console.log('Total hours:', duration.total('hours'));// Try changing largestUnit to 'hour' or 'minute'const byHour = start.until(end,{largestUnit:'hour'});console.log('largestUnit: hour ->', byHour.toString());
正在載入 Temporal polyfill…
Example 2: Calendar Support
The Chinese lunar calendar has months of variable length (29 or 30 days), and inserts a leap month in certain years. Adding "two months" with Date adds two Gregorian months, which shifts the lunar date. Temporal supports non-Gregorian calendars natively via the [u-ca=chinese] annotation — "add two months" operates within lunar calendar rules:
Try it: Chinese lunar calendar month arithmetic
TemporalJS
// Starting point: 2026-03-11 (Gregorian) = 22nd day, 1st lunar monthconsole.log('=== Starting point ===');const startISO ='2026-03-11';const startLegacy =newDate(2026,2,11);console.log('Gregorian:', startISO);console.log('Lunar: ', startLegacy.toLocaleDateString('zh-TW',{calendar:'chinese'}));console.log('');// 1. Date: adds two Gregorian months, lunar day shiftsconst legacyDate =newDate(2026,2,11);legacyDate.setMonth(legacyDate.getMonth()+2);console.log('=== Date +2 Gregorian months ===');console.log('Gregorian:', legacyDate.toLocaleDateString('en'));console.log('Lunar: ', legacyDate.toLocaleDateString('zh-TW',{calendar:'chinese'}),'← day shifted, not the 22nd');console.log('');// 2. Temporal without annotation: also adds Gregorian monthsconst plain =Temporal.PlainDate.from(startISO);const plainPlus2 = plain.add({months:2});console.log('=== Temporal without [u-ca=chinese] +2 months ===');console.log('Gregorian:', plainPlus2.toString());console.log('Lunar: ', plainPlus2.withCalendar('chinese').toLocaleString('zh-TW',{calendar:'chinese'}),'← also shifted');console.log('');// 3. Temporal with [u-ca=chinese]: adds lunar months correctlyconst lunar =Temporal.PlainDate.from(startISO +'[u-ca=chinese]');const lunarPlus2 = lunar.add({months:2});console.log('=== Temporal [u-ca=chinese] +2 lunar months ===');console.log('Gregorian:', lunarPlus2.toString());console.log('Lunar: ', lunarPlus2.toLocaleString('zh-TW',{calendar:'chinese'}),'← still the 22nd, correct');console.log('');// withCalendar(): convert any Gregorian date to lunar displayconst today =Temporal.Now.plainDateISO();console.log('=== Today ===');console.log('Gregorian:', today.toString());console.log('Lunar: ', today.withCalendar('chinese').toLocaleString('zh-TW',{calendar:'chinese'}));// Reverse: build a date from lunar componentsconst zhongyuan =Temporal.PlainDate.from({year:2026,month:7,day:15,calendar:'chinese'});console.log('');console.log('=== Ghost Festival (lunar 7th month, 15th day) ===');console.log('Lunar: ', zhongyuan.toLocaleString('zh-TW',{calendar:'chinese'}));console.log('Gregorian:', zhongyuan.withCalendar('iso8601').toString());
正在載入 Temporal polyfill…
Example 3: Instant for Cross-Timezone Display
Temporal.Instant represents an exact point in time with no timezone or calendar semantics — just nanoseconds since the Unix epoch. Store it, transmit it, then convert for each user's timezone:
Example 4: ZonedDateTime Handles DST Automatically
Many countries observe Daylight Saving Time (DST) — during transitions, some moments simply don't exist. Date leaves that problem to you; Temporal.ZonedDateTime handles it automatically.
On March 29, 2026, the UK clocks spring forward from 01:00 to 02:00, so 01:30 doesn't exist:
Try it: ZonedDateTime DST handling
TemporalJS
// UK spring clocks forward: 2026-03-29 01:00 → 02:00 (01:xx doesn't exist)const beforeDST =Temporal.ZonedDateTime.from('2026-03-29T00:30:00+00:00[Europe/London]');console.log('start:', beforeDST.toString());console.log('+1 hour:', beforeDST.add({hours:1}).toString());// Not 01:30 — Temporal skips straight to 02:30console.log('');console.log('+30 min:', beforeDST.add({minutes:30}).toString());console.log('+90 min:', beforeDST.add({minutes:90}).toString());// Timezone without DST (Taiwan) — no surprisesconst taipei =Temporal.ZonedDateTime.from('2026-03-29T00:30:00+08:00[Asia/Taipei]');console.log('');console.log('Taipei +1 hour:', taipei.add({hours:1}).toString());
正在載入 Temporal polyfill…
What Problems Does Temporal Solve?
Mutation hidden inside functions
Date is mutable. Passing a Date into a function can silently modify the original — no warning, no error.
With Date
Date vs Temporal: mutation
TemporalJS
// Date: mutation happens silentlyconst legacyDate =newDate('2026-02-25T00:00:00Z');functionaddOneDayLegacy(d){d.setDate(d.getDate()+1);// modifies the argument directlyreturn d;}console.log('Date before:', legacyDate.toISOString().slice(0,10));addOneDayLegacy(legacyDate);console.log('Date after: ', legacyDate.toISOString().slice(0,10));// mutated!console.log('');// Temporal: immutable — the original is always safeconst temporalDate =Temporal.PlainDate.from('2026-02-25');functionaddOneDayTemporal(d){return d.add({days:1});// returns a new object}console.log('Temporal before:', temporalDate.toString());addOneDayTemporal(temporalDate);console.log('Temporal after: ', temporalDate.toString());// unchanged
正在載入 Temporal polyfill…
Month arithmetic silently overflows
Date doesn't validate dates — it rolls overflow into the next month. Jan 31 + 1 month gives Mar 03 instead of Feb 28.
Temporal clamps to the last valid day of the month. Use overflow: 'reject' if you want an error instead.
Date vs Temporal: month overflow
TemporalJS
// Date: 1/31 + 1 month = 3/3 (silently skips February)const d =newDate('Jan 31 2026');d.setMonth(d.getMonth()+1);console.log('Date 1/31 + 1 month:', d.toDateString());// Mar 03console.log('');// Temporal: clamps to end of monthconst t =Temporal.PlainDate.from('2026-01-31');console.log('Temporal 1/31 + 1 month:', t.add({months:1}).toString());// 2026-02-28// Compare each month-endconst cases =['2026-01-31','2026-03-31','2026-10-31'];console.log('');cases.forEach(function(iso){const legacy =newDate(iso);legacy.setMonth(legacy.getMonth()+1);const temporal =Temporal.PlainDate.from(iso).add({months:1});console.log(iso,'+ 1 month');console.log(' Date:', legacy.toDateString(),' Temporal:', temporal.toString());});
正在載入 Temporal polyfill…
String parsing is inconsistent across browsers
The same "almost-ISO" string can produce different results depending on the browser — Chrome parses as local time, Firefox may throw, Safari parses as UTC. Temporal only accepts strictly defined formats and throws on anything ambiguous.
Date vs Temporal: string parsing
TemporalJS
// Date: behavior of this format was historically undefinedconst d =newDate('2026-06-25 15:15:00');if(isNaN(d)){console.log('Date result: Invalid Date');}else{console.log('Date getHours():', d.getHours(),'(local)');console.log('Date getUTCHours():', d.getUTCHours(),'(UTC)');console.log('→ parsed as:', d.getHours()===15?'local timezone':'not local timezone');}console.log('');// Temporal: ambiguous format throws immediatelytry{Temporal.PlainDateTime.from('2026-06-25 15:15:00');// space not allowed}catch(e){console.log('Temporal space-separated:', e.constructor.name,'(strict format required)');}// Correct formats:const t =Temporal.PlainDateTime.from('2026-06-25T15:15:00');// T separator requiredconsole.log('Temporal correct format:', t.toString());const tz =Temporal.ZonedDateTime.from('2026-06-25T15:15:00+08:00[Asia/Taipei]');console.log('With timezone:', tz.toString());
正在載入 Temporal polyfill…
These three problems together drove the entire datetime library ecosystem: by 2026, moment.js + dayjs + date-fns combined exceed 100 million weekly downloads.
Things Worth Knowing
1. Comparison requires compare(), not > / <
Temporal objects aren't primitives — > / < won't throw but always returns false. compare() returns -1, 0, or 1, and works directly with Array.sort.
Try it: compare() and sorting
TemporalJS
const dates =[Temporal.PlainDate.from('2026-05-01'),Temporal.PlainDate.from('2026-01-15'),Temporal.PlainDate.from('2026-12-31'),Temporal.PlainDate.from('2026-03-14'),];console.log('Before sort:', dates.map(d=> d.toString()));const sorted = dates.sort(Temporal.PlainDate.compare);console.log('After sort: ', sorted.map(d=> d.toString()));const a =Temporal.PlainDate.from('2026-01-01');const b =Temporal.PlainDate.from('2026-06-01');console.log('compare(a, b):',Temporal.PlainDate.compare(a, b));// -1
正在載入 Temporal polyfill…
2. dayOfWeek is ISO 8601 — not Date.getDay()
Date.getDay() returns 0 for Sunday and 6 for Saturday. Temporal uses ISO 8601: 1 is Monday, 7 is Sunday. Watch out when porting old code.
Try it: dayOfWeek vs Date.getDay()
TemporalJS
const dates =['2026-03-15','2026-03-16','2026-03-17','2026-03-20','2026-03-21'];dates.forEach(function(iso){const t =Temporal.PlainDate.from(iso);const d =newDate(iso +'T12:00:00Z');console.log(iso,' Temporal.dayOfWeek:', t.dayOfWeek,' Date.getDay():', d.getUTCDay());});console.log('');console.log('Temporal: 1=Mon 2=Tue ... 6=Sat 7=Sun');console.log('Date: 0=Sun 1=Mon ... 5=Fri 6=Sat');constisWeekend=(d)=> d.dayOfWeek>=6;// correct for Temporalconst sat =Temporal.PlainDate.from('2026-03-21');const sun =Temporal.PlainDate.from('2026-03-22');console.log('');console.log('Sat isWeekend:',isWeekend(sat));// trueconsole.log('Sun isWeekend:',isWeekend(sun));// true
正在載入 Temporal polyfill…
3. Duration doesn't auto-balance
Temporal.Duration preserves the units you give it — 100 seconds stays PT100S, not PT1M40S. Call round({ largestUnit: 'hour' }) when you need normalization.
6. DST gaps when converting PlainDateTime to ZonedDateTime
Converting a wall-clock time to a specific timezone can hit a DST gap. Temporal requires you to choose how to handle it: reject throws, earlier picks the time before the gap, later picks after, compatible (default) equals later.
This only applies when constructing — arithmetic on an existing ZonedDateTime handles gaps automatically.
Try it: PlainDateTime disambiguation
TemporalJS
// 2026-03-29 01:30 doesn't exist in London (DST gap)const pdt =Temporal.PlainDateTime.from('2026-03-29T01:30:00');try{pdt.toZonedDateTime('Europe/London',{disambiguation:'reject'});}catch(e){console.log('reject:', e.constructor.name,"(moment doesn't exist)");}const earlier = pdt.toZonedDateTime('Europe/London',{disambiguation:'earlier'});const later = pdt.toZonedDateTime('Europe/London',{disambiguation:'later'});console.log('earlier:', earlier.toString());console.log('later: ', later.toString());// ZonedDateTime arithmetic avoids this entirelyconst zdt =Temporal.ZonedDateTime.from('2026-03-29T00:30:00+00:00[Europe/London]');console.log('');console.log('ZDT +1h (auto-skips gap):', zdt.add({hours:1}).toString());
正在載入 Temporal polyfill…
7. Instant has no year, month, or day
Temporal.Instant is just a nanosecond timestamp — no calendar, no timezone. To access date fields, convert first with .toZonedDateTimeISO(tz).
Try it: Instant has no date fields
TemporalJS
const inst =Temporal.Instant.from('2026-03-14T10:00:00Z');console.log('epochMilliseconds:', inst.epochMilliseconds);console.log('epochNanoseconds:', inst.epochNanoseconds.toString());console.log('year:', inst.year);// undefinedconsole.log('month:', inst.month);// undefinedconsole.log('');const zones =['UTC','Asia/Taipei','America/New_York'];zones.forEach(function(tz){const zdt = inst.toZonedDateTimeISO(tz);console.log(tz +': '+ zdt.year+'-'+String(zdt.month).padStart(2,'0')+'-'+String(zdt.day).padStart(2,'0')+' '+String(zdt.hour).padStart(2,'0')+':00');});console.log('');// Same Instant, different calendar date depending on timezoneconst midnight =Temporal.Instant.from('2026-03-14T23:30:00Z');const tpe = midnight.toZonedDateTimeISO('Asia/Taipei');const nyc = midnight.toZonedDateTimeISO('America/New_York');console.log('Same instant — Taipei:', tpe.toPlainDate().toString(),' New York:', nyc.toPlainDate().toString());
正在載入 Temporal polyfill…
8. The DST day is only 23 hours long
When clocks spring forward, the day from midnight to midnight is only 23 hours. ZonedDateTime.until() reflects that accurately — you get PT23H, not PT24H.
.add({ hours: 24 }) and .add({ days: 1 }) produce different results on that day:
Try it: 23-hour DST day
TemporalJS
const before =Temporal.ZonedDateTime.from('2026-03-29T00:00:00[Europe/London]');const after =Temporal.ZonedDateTime.from('2026-03-30T00:00:00[Europe/London]');console.log('DST day length:', before.until(after,{largestUnit:'hour'}).toString());// PT23H, not PT24Hconst morning =Temporal.ZonedDateTime.from('2026-03-29T09:00:00[Europe/London]');console.log('');console.log('+ 24 hours:', morning.add({hours:24}).toString());// next day 10:00console.log('+ 1 day: ', morning.add({days:1}).toString());// next day 09:00
正在載入 Temporal polyfill…
9. PlainMonthDay for recurring annual events
For events like holidays that repeat on the same date each year, use Temporal.PlainMonthDay and call toPlainDate({ year }) to get the actual date for any year.
Try it: PlainMonthDay for annual events
TemporalJS
const dayNames =['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];// Christmas: December 25 — what day of the week?const xmas =Temporal.PlainMonthDay.from('12-25');console.log('Christmas 2026–2035:');for(let year =2026; year <=2035; year++){const date = xmas.toPlainDate({ year });console.log(year +':', date.toString(), dayNames[date.dayOfWeek-1]);}console.log('');// Days until Christmasconst today =Temporal.Now.plainDateISO();let nextXmas = xmas.toPlainDate({year: today.year});if(Temporal.PlainDate.compare(nextXmas, today)<0){nextXmas = xmas.toPlainDate({year: today.year+1});}console.log('Today:', today.toString());console.log('Days until Christmas:', today.until(nextXmas).days);
正在載入 Temporal polyfill…
10. Interoperating with Date
If you're mixing Temporal with legacy Date code:
Date → Temporal: Temporal.Instant.fromEpochMilliseconds(legacy.getTime()) (or legacy.toTemporalInstant() in native ES2026)
Temporal → Date: new Date(instant.epochMilliseconds)
Precision drops from nanoseconds to milliseconds, and calendar/timezone metadata is lost.
Temporal became part of ES2026 in March 2026 and is natively shipped in modern browsers:
Chrome 144+ (since January 2026)
Firefox 139+ (since May 2025)
Edge 144+ (since January 2026)
Safari (partial support in Tech Preview)
If you need to support older browsers, use @js-temporal/polyfill:
npm install @js-temporal/polyfill
import { Temporal } from'@js-temporal/polyfill';
Wrapping Up
Date shipped in 1995 as a rushed copy of Java's java.util.Date — an API Java itself eventually deprecated for the same reasons. For thirty years, handling anything beyond basic date display in JavaScript meant pulling in a library.
Temporal isn't a patch. It's a full redesign: immutable objects, a proper type system where Instant and PlainDate represent genuinely different concepts, strict format parsing, complete IANA timezone support, and multi-calendar arithmetic. Everything that used to require a third-party package is now built in.
The most immediate impact for frontend work: fewer date-related bugs, full stop. Silent mutation, month overflow, and cross-browser parsing differences account for a large fraction of datetime bugs in the wild. Temporal closes all three at the language level.
Chrome 144+, Firefox 139+, and Edge 144+ ship it natively. Safari is catching up. You can start today with the official polyfill and drop it once browser support is complete.