Your browser’s native dates in plain English
Temporal gave the browser real time zones, daylight saving and calendar math. Native Dates gives it an API to match: four nouns, verbs you can read aloud and values that never change under you, in 7.3 kB br.
npm i native-datestoday()CalendarDateNov 11, 2026.next('monday').next('wednesday').next('friday').next('sunday')CalendarDateNov 16, 2026.at('9:30am', 'America/Los_Angeles').at('2pm', 'America/New_York').at('8am', 'Europe/London').at('6:45pm', 'Asia/Tokyo')DateTimeNov 16, 2026, 9:30 AM PST.in('Europe/Berlin').in('Asia/Kolkata').in('Australia/Sydney').in('America/Sao_Paulo')DateTimeNov 16, 2026, 6:30 PM GMT+1today()CalendarDateNov 11, 2026.plus(1, 'month').plus(2, 'weeks').plus(90, 'days').plus(1, 'year')CalendarDateDec 11, 2026.endOf('month').endOf('week').endOf('quarter').endOf('year')CalendarDateDec 31, 2026.format('full').format('long').format('short')string'Thursday, December 31, 2026'duration('1h 30m')duration('45 minutes')duration('2 days and 6 hours')Duration1 hour, 30 minutes.plus(45, 'minutes').plus(3, 'hours').plus(1, 'day')Duration2 hours, 15 minutes.times(2).times(3)Duration4 hours, 30 minutes.format('narrow').format('short').format('digital')string'4h 30m'Date gets these wrong. Temporal gets them right.
Four questions every app asks. Date has answered them this way since 1995, and your code has worked around it ever since. Native Dates hands each one to Temporal, and logs a value you can read without opening it.
What day is 2026-09-01 in New York?
Date reads a bare date as midnight in UTC, which is still the evening before in New York. A calendar date has no clock to shift, so it is September 1 everywhere.
const day = new Date('2026-09-01');day.toLocaleDateString('en-US', { timeZone: 'America/New_York' });'8/31/2026' a day early
date('2026-09-01');CalendarDate {text: 'Sep 1, 2026', month: 9, day: 1, year: 2026, weekday: 2}
What is one month after January 31?
Date counts to February 31 and spills into March. Temporal stops at the last day February has.
const end = new Date(2026, 0, 31);
end.setMonth(1);
end.toDateString();
'Tue Mar 03 2026' three days late
date('2026-01-31').plus(1, 'month');CalendarDate {text: 'Feb 28, 2026', month: 2, day: 28, year: 2026, weekday: 6}
It is 9 AM on Saturday in New York. What time is it one day later?
The clocks spring forward that night, so the day is 23 hours long. Outside its own zone, Date can only add 24 hours. Temporal adds a day and keeps the clock at nine.
const nine = new Date('2026-03-07T09:00-05:00');const next = new Date(nine.getTime() + 24 * 60 * 60 * 1000);
next.toLocaleTimeString('en-US', { timeZone: 'America/New_York' });'10:00:00 AM' an hour late
datetime('2026-03-07T09:00', 'America/New_York').plus(1, 'day');DateTime {text: 'Mar 8, 2026, 9:00 AM EDT', month: 3, day: 8, year: 2026, weekday: 7, …}
The due date is 30 days after the start. What is the start now?
setDate() changes the date it is called on, and due is the same object as start. A Temporal value never changes, so plus() hands back a new one.
const start = new Date(2026, 8, 1);
const due = start;
due.setDate(31);
start.toDateString();
'Thu Oct 01 2026' the start moved
const start = date('2026-09-01');const due = start.plus(30, 'days');
start;
CalendarDate {text: 'Sep 1, 2026', month: 9, day: 1, year: 2026, weekday: 2}
Every zone and every clock change, already in your browser
Los Angeles and Berlin change their clocks on different weekends, so the hours between them change 4 times a year. Date knows two zones, yours and UTC. Temporal knows every zone on every date, so a standup set in one city lands at the right time in the other.
Code you can read aloud
Every call is one word for one thought, in the order you would say it. Switch to Temporal to read the same dates written against the platform.
- One word per thought
next('monday')finds the day, andat()gives it a time and a place. On Temporal that is weekday arithmetic and an options bag. - A zone is a view
in()reads the same moment on another clock, andformat('full')writes it the way the reader’s locale does. - Calendar math by name
endOf('month')is the 28th, the 30th or the 31st, without asking how long the month is. - Durations in words
until()reads both moments in one zone, andformat()prints the wait in the reader’s language.
import { now, today } from 'native-dates';
!!const standup = today().next('monday').at('9:30am', 'America/Los_Angeles');
!!const invite = standup.in('Europe/Berlin').format('full');
!!const due = today().plus(1, 'month').endOf('month');
const notice = due.format('long');
!!const wait = now().until(standup).round('minute').format();const today = Temporal.Now.plainDateISO();
// the next monday strictly after today
const ahead = (8 - today.dayOfWeek) % 7 || 7;
const standup = today.add({ days: ahead }).toZonedDateTime({
timeZone: 'America/Los_Angeles',
plainTime: Temporal.PlainTime.from('09:30'),
});
const invite = standup.withTimeZone('Europe/Berlin').toLocaleString(undefined, {
dateStyle: 'full',
timeStyle: 'short',
});
const nextMonth = today.add({ months: 1 });
const due = nextMonth.with({ day: nextMonth.daysInMonth });
const notice = due.toLocaleString(undefined, { dateStyle: 'long' });
// until() refuses two zones once days are involved, so both sides read in this one
const here = Temporal.Now.zonedDateTimeISO();
const until = here.until(standup.withTimeZone(here.timeZoneId), {
largestUnit: 'year',
smallestUnit: 'minute',
roundingMode: 'halfExpand',
});
const wait = new Intl.DurationFormat(undefined, { style: 'long' }).format(until);Both programs give the same three results. The build that made this page ran them to check.
Run it as if you were in Tokyo
A small app, built and run in your browser. Pick a zone in the preview’s bar and it runs as it would there. Change the code and it rebuilds as you type.
import { tomorrow } from 'native-dates';
import { CITIES } from './cities.js';
const input = document.querySelector('input');
const summary = document.querySelector('#summary');
const list = document.querySelector('ul');
function render() {
// tomorrow, at the time typed, where this app is running
const meeting = tomorrow().at(input.value);
summary.textContent = meeting.format('full');
list.innerHTML = CITIES.map((zone) => {
const there = meeting.in(zone);
const sameDay = there.date.equals(meeting.date);
return `<li>
<code>${zone}</code>
<b>${there.format('time')}</b>
<span class="muted">${sameDay ? '' : there.format({ weekday: 'long' })}</span>
</li>`;
}).join('');
}
input.addEventListener('input', render);
render();
Starting…
The whole core is 7.3 kB
Four nouns, every verb, formatting and zones, with no dependencies. A plugin adds one more thing when you import it. Switch them on to see what your app would ship.
Start with now()
now().text
npm i native-dates