react-time-formatter
v1.0.2
Published
React-first date & time formatting library with a clear API and built-in timezone support
Maintainers
Readme
React-first date & time formatting library with a clear API and built-in timezone support
Features
- 📅
DateTime— A consistent wrapper aroundDatewith 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
Example
Just write what you mean—the API stays out of your way.
<Duration of={{ minutes: 80 }}>
{t => <span>{t.HH} hours {t.mm} minutes</span>}
</Duration>→ 1 hours 20 minutes
Automatically falls back to smaller units when larger ones aren’t requested:
<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
Consistent and non-misleading: shows placeholders instead of correct-looking false values.
<DateTime at={new Date(1e15)}>
{dt => <span>{dt.hh}:{dt.mm} {dt.A} {dt.MM}/{dt.DD} {dt.YYYY}</span>}
</DateTime>→ ##:## -- ##/## ####
Setup
Installation
npm install react-time-formatterESM import
import { Interval } from 'react-time-formatter'CJS import
const { Interval } = require('react-time-formatter/Interval');Guideline
There's only one guideline.
Avoid early or unnecessary dereference
❌ 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
API Reference
DateTime
DateTime is a specific point on the timeline.
Props
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)
Output
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
Pre-formatted keys
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" | "####" |
Raw keys
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
Duration is a fixed quantity of time units, independent of specific dates or positions on a timeline.
❗ For Duration
yearsare approximated to 365 days andmonthsare approximated to30 days. Use Interval if these units need to be precisely reflected.
Props
of:numberRelativeTime— The quantity of time in milliseconds or arbitrary unitschildren?:Render— Render functionrender?:Render— Render function (alias forchildren)
Output
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
Pre-formatted keys
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" | "###" |
Raw unit values
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 | |
Total values
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
Interval is a calendar-aware span between two dates. Unlike Duration Interval accounts for the variable length of months and years.
Props
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)
Output
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
Pre-formatted keys
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" | "###" |
Raw unit values
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 |
Total values
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 |
Timezone
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.
Pre-made timezones
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!
To statically apply a pre-made timezone you can import it from its tz/ path:
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!
Automatic timezones
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.
To create an automatic timezone, just pass its IANA name:
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!
Invalid timezones will never produce a valid-looking result:
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
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.
The simplest custom timezone:
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
The custom implementation of the local timezone:
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();The custom implementation of the Berlin timezone with its DST rules:
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!
Type reference
CalendarDate
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,
}RelativeTime
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
}License
MIT © 2025 Nikolai Laevskii
