React-first date & time formatting library with a clear API and built-in timezone support
- 📅
DateTime
— A consistent wrapper aroundDate
with zoned time support - ⏱️
Duration
— A fixed time span that overflows into larger units automatically - 🗓️
Interval
— Calendar-aware difference between two dates - 🌍
tz
— Timezone engine with sensible built-in zones and override support - 📦 Fully tree-shakable with both ESM and CJS builds
- 💙 TypeScript-native with full static type safety
- 🚫 Zero-dependency
<Duration of={{ minutes: 80 }}>
{t => <span>{t.HH} hours {t.mm} minutes</span>}
</Duration>
→ 1 hours 20 minutes
<Interval from='1789-07-14' to='1799-11-09'>
{t => <span>{t.YY} years {t.MM} months {t.DD} days</span>}
</Interval>
→ 10 years 03 months 26 days
<Interval from='1789-07-14' to='1799-11-09'>
{t => <span>{t.DD} days</span>}
</Interval>
→ 3770 days
<DateTime at={new Date(1e15)}>
{dt => <span>{dt.hh}:{dt.mm} {dt.A} {dt.MM}/{dt.DD} {dt.YYYY}</span>}
</DateTime>
→ ##:## -- ##/## ####
Installation
npm install react-time-formatter
ESM import
import { Interval } from 'react-time-formatter'
CJS import
const { Interval } = require('react-time-formatter/Interval');
There's only one guideline.
❌ Incorrect:
Although this may seem to work fine, HH
, mm
and ss
occur before any logic happens which may lead to incorrect results in some cases.
<Interval from={DateA} to={DateB}>
{({ HH, mm, ss }) => <span>{HH}:{mm}:{ss}</span>}
</Interval>
✅ Correct: Safe. Each unit is referenced only when it's needed.
<Interval from={DateA} to={DateB}>
{t => <span>{t.HH}:{t.mm}:{t.ss}</span>}
</Interval>
❌ Incorrect:
Whenever unit is referenced (e.g. t.hours
) it's assumed to be used.
<Interval from={DateA} to={DateB}>
{t => {
if (t.hours < 2) return <span>{t.mm}:{t.ss}</span>
return (<span>{t.HH}:{t.mm}:{t.ss}</span>)
}}
</Interval>
For 80 minutes will output → 20:00
(since hours are referenced at t.hours
will assume "01:20:00" is being rendered)
✅ Correct:
Use total*
units for conditions when applicable:
<Interval from={DateA} to={DateB}>
{t => {
if (t.totalHours < 2) return <span>{t.mm}:{t.ss}</span>
return (<span>{t.HH}:{t.mm}:{t.ss}</span>)
}}
</Interval>
For 80 minutes will correctly render → 80:00
✅ Also correct: Use units that are gonna be used under either condition:
<Interval from={DateA} to={DateB}>
{t => {
if (t.minutes < 2) return <span>{t.mm}:{t.ss}</span>
return (<span>{t.HH}:{t.mm}:{t.ss}</span>)
}}
</Interval>
For 80 minutes will correctly render → 80:00
DateTime
is a specific point on the timeline.
at:
number
|string
|Date
|CalendarDate
— The date to be displayed.timezone?:
'UTC'
|'Local'
|number
|Timezone
— The timezone in which the date will be displayed.children?:
Render — Render functionrender?:
Render — Render function (alias forchildren
)
Passed to render function and abbreviated as dt
in this doc.
<DateTime at={new Date('2025-02-05T15:08:09.998')}>
{dt => <span>{dt.hh}:{dt.mm} {dt.A} {dt.MM}/{dt.DD} {dt.YYYY}</span>}
</DateTime>
→ 03:08 PM 02/05 2025
Recommended for insertion, since they're already of string
type and support readable error-state in form of #
or --
symbols for cases when correct date can't be displayed.
Token | Type | Description | Example | Invalid |
---|---|---|---|---|
dt.YY |
string |
2-digit year | "25" |
"##" |
dt.YYYY |
string |
4-digit year | "2025" |
"####" |
dt.M |
string |
Month (1–12) | "2" (February) |
"#" |
dt.MM |
string |
2-digit month | "02" |
"##" |
dt.D |
string |
Day of the month (1–31) | "5" |
"#" |
dt.DD |
string |
2-digit day of the month | "05" |
"##" |
dt.d |
string |
Day of week (1 = Monday) | "1" |
"#" |
dt.H |
string |
Hour (24h) | "15" |
"##" |
dt.HH |
string |
2-digit hour (24h) | "15" |
"##" |
dt.h |
string |
Hour (12h) | "3" |
"#" |
dt.hh |
string |
2-digit hour (12h) | "03" |
"##" |
dt.m |
string |
Minute | "8" |
"#" |
dt.mm |
string |
2-digit minute | "08" |
"##" |
dt.s |
string |
Second | "9" |
"#" |
dt.ss |
string |
2-digit second | "09" |
"##" |
dt.SSS |
string |
Milliseconds (3-digit) | "998" |
"###" |
dt.A |
string |
Meridiem (uppercase) | "AM" / "PM" |
"--" |
dt.a |
string |
Meridiem (lowercase) | "am" / "pm" |
"--" |
dt.Z |
string |
UTC offset (+hh:mm or Z ) |
"+10:00" |
"##:##" |
dt.ZZ |
string |
UTC offset (+hhmm ) |
"+1000" |
"####" |
Are always of number
type. Recommended for conditional logic and debugging, will turn into NaN
when the input is incorrect.
Token | Type | Example | Note |
---|---|---|---|
dt.year |
number |
2025 |
|
dt.month |
number |
2 (february) |
1-indexed |
dt.date |
number |
5 |
|
dt.day |
number |
1 |
Day of the week (1 = Monday) |
dt.hours |
number |
15 |
|
dt.minutes |
number |
8 |
|
dt.seconds |
number |
9 |
|
dt.milliseconds |
number |
998 |
|
dt.timezoneOffset |
number |
-600 |
In minutes: for example -180 means UTC+3 |
Duration
is a fixed quantity of time units, independent of specific dates or positions on a timeline.
❗ For Duration
years
are approximated to 365 days andmonths
are approximated to30 days
. Use Interval if these units need to be precisely reflected.
of:
number
RelativeTime
— The quantity of time in milliseconds or arbitrary unitschildren?:
Render
— Render functionrender?:
Render
— Render function (alias forchildren
)
Passed to render function and abbreviated as t
in this doc.
<Duration of={4800000}>
{t => <span>{t.HH} hours {t.mm} minutes</span>}
</Duration>
→ 01 hours 20 minutes
Recommended for insertion, since they're already of string
type and support readable error-state in form of #
symbols for cases when correct duration can't be displayed.
Unit | Type | Description | Example | Invalid |
---|---|---|---|---|
t.Y |
string |
Years (1–2 digits) | "5" |
"#" |
t.YY |
string |
2-digit years | "05" |
"##" |
t.M |
string |
Months (1–2 digits) | "2" |
"#" |
t.MM |
string |
2-digit months | "02" |
"##" |
t.W |
string |
Weeks (1–2 digits) | "2" |
"#" |
t.WW |
string |
2-digit weeks | "02" |
"##" |
t.D |
string |
Days (1–2 digits) | "4" |
"#" |
t.DD |
string |
2-digit days | "04" |
"##" |
t.H |
string |
Hours (1–2 digits) | "15" |
"##" |
t.HH |
string |
2-digit hours | "15" |
"##" |
t.m |
string |
Minutes (1–2 digits) | "8" |
"#" |
t.mm |
string |
2-digit minutes | "08" |
"##" |
t.s |
string |
Seconds (1–2 digits) | "9" |
"#" |
t.ss |
string |
2-digit seconds | "09" |
"##" |
t.SSS |
string |
Milliseconds (3 digits) | "998" |
"###" |
Are always of number
type and either positive or 0, never negative. Recommended for conditional logic and debugging, will turn into NaN
when the input is incorrect.
Unit | Type | Example | Note |
---|---|---|---|
t.years |
number |
5 |
For Duration years are approximated to 365 days |
t.months |
number |
2 |
For Duration months are approximated to 30 days |
t.weeks |
number |
2 |
Is defined as chunk of 7 days |
t.days |
number |
4 |
|
t.hours |
number |
15 |
|
t.minutes |
number |
8 |
|
t.seconds |
number |
9 |
|
t.milliseconds |
number |
998 |
Independent from the rest of keys and their insertion doesn't affect formatting rules. Each key represents the whole duration converted to a single unit. Are always of number
type and can be both positive or negative or 0. Will also turn into NaN
when the input is incorrect.
Key | Type | Example | Note |
---|---|---|---|
t.totalYears |
number |
5 |
For Duration years are approximated to 365 days |
t.totalMonths |
number |
62 |
For Duration months are approximated to 365 days |
t.totalWeeks |
number |
271 |
|
t.totalDays |
number |
1903 |
|
t.totalHours |
number |
45687 |
|
t.totalMinutes |
number |
2741228 |
|
t.totalSeconds |
number |
164473689 |
|
t.totalMilliseconds |
number |
164473689998 |
Interval
is a calendar-aware span between two dates. Unlike Duration Interval accounts for the variable length of months
and years
.
from:
number
|string
|Date
|CalendarDate
— Starting point in timeto:
number
|string
|Date
|CalendarDate
— Ending point in time (non-inclusive)timezone?:
'UTC'
|'Local'
|number
|Timezone
— The timezone in which the difference will be calculatedchildren?:
Render
— Render functionrender?:
Render
— Render function (alias forchildren
)
Passed to render function and abbreviated as t
in this doc.
<Interval from={new Date('1789-07-14')} to={new Date('1799-11-09')}>
{t => <span>{t.YY} years {t.MM} months {t.DD} days</span>}
</Interval>
→ 10 years 03 months 26 days
Recommended for insertion, since they're already of string
type and support readable error-state in form of #
symbols for cases when correct duration can't be displayed.
Unit | Type | Description | Example | Invalid |
---|---|---|---|---|
t.Y |
string |
Years (1–2 digits) | "5" |
"#" |
t.YY |
string |
2-digit years | "05" |
"##" |
t.M |
string |
Months (1–2 digits) | "2" |
"#" |
t.MM |
string |
2-digit months | "02" |
"##" |
t.W |
string |
Weeks (1–2 digits) | "2" |
"#" |
t.WW |
string |
2-digit weeks | "02" |
"##" |
t.D |
string |
Days (1–2 digits) | "4" |
"#" |
t.DD |
string |
2-digit days | "04" |
"##" |
t.H |
string |
Hours (1–2 digits) | "15" |
"##" |
t.HH |
string |
2-digit hours | "15" |
"##" |
t.m |
string |
Minutes (1–2 digits) | "8" |
"#" |
t.mm |
string |
2-digit minutes | "08" |
"##" |
t.s |
string |
Seconds (1–2 digits) | "9" |
"#" |
t.ss |
string |
2-digit seconds | "09" |
"##" |
t.SSS |
string |
Milliseconds (3 digits) | "998" |
"###" |
Are always of number
type and either positive or 0, never negative. Recommended for conditional logic and debugging, will turn into NaN
when the input is incorrect.
Unit | Type | Example |
---|---|---|
t.years |
number |
5 |
t.months |
number |
2 |
t.weeks |
number |
2 |
t.days |
number |
4 |
t.hours |
number |
15 |
t.minutes |
number |
8 |
t.seconds |
number |
9 |
t.milliseconds |
number |
998 |
Independent from the rest of keys and their insertion doesn't affect formatting rules. Each key represents the whole duration converted to a single unit. Are always of number
type and can be both positive or negative or 0. Will also turn into NaN
when the input is incorrect.
Key | Type | Example |
---|---|---|
t.totalYears |
number |
5 |
t.totalMonths |
number |
62 |
t.totalWeeks |
number |
271 |
t.totalDays |
number |
1903 |
t.totalHours |
number |
45687 |
t.totalMinutes |
number |
2741228 |
t.totalSeconds |
number |
164473689 |
t.totalMilliseconds |
number |
164473689998 |
Timezones are applicable to DateTime
or Interval
components. The only correct way to apply timezone is via timezone
prop. Having timezone in the date-string itself DOES NOT affect how the date will be displayed. Without timezone
prop the timezone defaults to local.
❌ Incorrect: Setting timezone right inside the date string. It will only affect how the date will be interpreted.
<DateTime at='2025-02-05T12:00+01:00'>
{dt => <span>It's {dt.HH}:{dt.mm} in Belgrade!</span>}
</DateTime>
✅ Correct:
Applying timezone via timezone
prop:
import { DateTime } from 'react-time-formatter';
import Belgrade from 'react-time-formatter/tz/Europe/Belgrade'
You can still put timezone offset inside the date string, just keep in mind it will only affect parsing process and not formatting.
<DateTime at='2025-02-05T12:00+01:00' timezone={Belgrade}>
{dt => <span>It's {dt.HH}:{dt.mm} in Belgrade!</span>}
</DateTime>
→ It's 12:00 in Belgrade!
❗ Pre-made and automatic timezones are dependent on Int.DateTimeFormat API which is widely available across browsers. But if you need even more control, refer to custom timezones section.
The most basic pre-made timezones can be enabled by just strings, these timezones include: "Local"
(also is default) and "UTC"
:
<DateTime at={Date.now()} timezone="UTC">
{dt => <span>It's {dt.HH}:{dt.mm} UTC!</span>}
</DateTime>
→ It's 11:00 UTC!
Each path corresponds to the IANA name:
import { DateTime } from 'react-time-formatter';
import Belgrade from 'react-time-formatter/tz/Europe/Belgrade'
Just pass it to timezone prop:
<DateTime at={Date.now()} timezone={Belgrade}>
{dt => <span>It's {dt.HH}:{dt.mm} in Belgrade!</span>}
</DateTime>
→ It's 12:00 in Belgrade!
If you want to apply the timezone dynamically or the desired timezone is absent from the pre-made timezones list, you can use createTimezone
helper that will create a timezone for you.
import { DateTime } from 'react-time-formatter';
import { createTimezone } from 'react-time-formatter/tz/createTimezone'
const Tokyo = createTimezone('Asia/Tokyo');
Similarly, just pass it to timezone prop:
<DateTime at={Date.now()} timezone={Tokyo}>
{dt => <span>It's {dt.HH}:{dt.mm} in Tokyo!</span>}
</DateTime>
→ It's 10:00 in Tokyo!
import { DateTime } from 'react-time-formatter';
import { createTimezone } from 'react-time-formatter/tz/createTimezone'
const What = createTimezone('Invalid/Unknown');
<DateTime at={Date.now()} timezone={What}>
{dt => <span>It's always {dt.HH}:{dt.mm} in the invalid timezone!</span>}
</DateTime>
→ It's always ##:## in the invalid timezone!
Custom timezones are defined by a TimezoneOffsetResolver function (date: Date) => number
which takes single Date
argument and returns timezone offset in minutes. Static timezones (with no DST) may also be just defined as number.
Just a static UTC+3 offset:
const UTC3 = () => -180;
Also valid:
const UTC3 = -180;
Note that -180
translates into UTC+3
. Timezone offset is consistent with JS Date.prototype.getTimezoneOffset()
function that defines timezone offset as how many minutes should be added to local timezone to get UTC.
<DateTime at={Date.now()} timezone={UTC3}>
{dt => <span>UTC{t.ZZ} time is {dt.HH}:{dt.mm}</span>}
</DateTime>
→ UTC+0300 time is 14:00
Local timezone is applied by default so you don't normally have to implement it. But if you had to, it would look like this:
const Local = (date: Date) => date.getTimezoneOffset();
In case you can't rely on browser's timezone implementation and need your timezone to always have the same behaviour, you can define your all DST rules explicitly:
// The manual equivalent of createTimezone('Europe/Berlin') call:
const Berlin = (date: Date): number => {
const year = date.getUTCFullYear();
// Find last Sunday in March
const startDST = new Date(Date.UTC(year, 2, 31, 1)); // March 31, 01:00 UTC
while (startDST.getUTCDay() !== 0) {
startDST.setUTCDate(startDST.getUTCDate() - 1);
}
// Find last Sunday in October
const endDST = new Date(Date.UTC(year, 9, 31, 1)); // October 31, 01:00 UTC
while (endDST.getUTCDay() !== 0) {
endDST.setUTCDate(endDST.getUTCDate() - 1);
}
// If date is within DST range, return UTC+2 = -120
if (date >= startDST && date < endDST) {
return -120;
}
// Otherwise, return UTC+1 = -60
return -60;
};
<DateTime at={Date.now()} timezone={Berlin}>
{dt => <span>It's {dt.HH}:{dt.mm} in Berlin!</span>}
</DateTime>
→ It's 01:00 in Berlin!
Can act as input for: DateTime, Interval Matching type is returned by: DateTime
A flat object representation for date. Must include at least year value. dt
is guaranteed to be valid CalendarDate.
// year and month, all the rest values will be set to 0
const valid = {
year: 2025,
// 1-indexed, february
month: 2,
date: 15,
};
// meaningless without year
const invalid1 = {
month: 2,
date: 15
};
// should have at least year value
const invalid2 = {};
const full = {
year: 2025,
month: 2,
date: 15,
hours: 10,
minutes: 0,
seconds: 15,
milliseconds: 12,
// UTC+3. Since CalendarDate is just a snapshot, the timezone offset can only be a number.
// Doesn't affect how the date will be displayed, only how it will be parsed.
timezoneOffset: -180,
}
Can act as input for: Duration Matching type is returned by: Duration, Interval
A flat representation of quantity of time. Must include at least one time unit to be valid. t
is guaranteed to be valid RelativeTime.
// can't be empty object
const invalid = {}
// can have any units in any quantities
const valid = { minutes: 80 }
// with all units
const full = {
years: 9,
months: 10,
weeks: 3,
days: 4,
hours: 5,
minutes: 7,
seconds: 15,
milliseconds: 900
}
MIT © 2025 Nikolai Laevskii