When a comment timestamp reads "3 hours ago" instead of a raw ISO string, users stay oriented without thinking about it. Most teams reach for date-fns/formatDistanceToNow or moment().fromNow() to get there. Both pull in a full library for something the browser can do natively — and has been able to do since 2020.
The API
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-3, 'hour'); // "3 hours ago"
rtf.format(1, 'day'); // "tomorrow"
rtf.format(-1, 'day'); // "yesterday"
rtf.format(-2, 'week'); // "2 weeks ago"
rtf.format(3, 'month'); // "in 3 months"
Two arguments: a number (negative = past, positive = future) and a unit string. The constructor takes a locale identifier and an options object. That's the whole surface area.
The numeric: 'auto' option
The second constructor argument controls whether you get "1 day ago" or "yesterday". With numeric: 'always' (the default), every value is formatted as a number. With numeric: 'auto', the formatter substitutes natural language when it's available for that locale:
const always = new Intl.RelativeTimeFormat('en', { numeric: 'always' });
const auto = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
always.format(-1, 'day'); // "1 day ago"
auto.format(-1, 'day'); // "yesterday"
always.format(0, 'day'); // "in 0 days"
auto.format(0, 'day'); // "today"
always.format(1, 'day'); // "in 1 day"
auto.format(1, 'day'); // "tomorrow"
In most UIs, numeric: 'auto' is what you want. Saying "yesterday" is more natural than "1 day ago" and it costs nothing extra.
Picking the right unit automatically
The API doesn't pick the unit for you — you pass 'hour' or 'week' explicitly. That means you need a helper that looks at the elapsed duration and decides which unit to use. Ten lines:
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
const THRESHOLDS = [
{ unit: 'year', ms: 365 * 24 * 60 * 60 * 1000 },
{ unit: 'month', ms: 30 * 24 * 60 * 60 * 1000 },
{ unit: 'week', ms: 7 * 24 * 60 * 60 * 1000 },
{ unit: 'day', ms: 24 * 60 * 60 * 1000 },
{ unit: 'hour', ms: 60 * 60 * 1000 },
{ unit: 'minute', ms: 60 * 1000 },
{ unit: 'second', ms: 1000 },
];
function timeAgo(date) {
const diffMs = date - Date.now();
for (const { unit, ms } of THRESHOLDS) {
if (Math.abs(diffMs) >= ms) {
return rtf.format(Math.round(diffMs / ms), unit);
}
}
return rtf.format(0, 'second'); // "just now"
}
timeAgo(new Date(Date.now() - 2 * 60 * 60 * 1000)); // "2 hours ago"
timeAgo(new Date(Date.now() + 3 * 24 * 60 * 60 * 1000)); // "in 3 days"
timeAgo(new Date(Date.now() - 45 * 1000)); // "45 seconds ago"
You adjust the thresholds to your app's conventions — some UIs show minutes up to 90, others switch to hours at 60. The formatter handles the phrasing; the thresholds handle the unit selection.
Locale support comes for free
The formatter respects locale — the same code that prints "3 hours ago" in English prints "il y a 3 heures" in French, "vor 3 Stunden" in German, and "۳ ساعت پیش" in Persian, without any translation strings in your bundle.
const locales = ['en', 'fr', 'de', 'ja', 'ar'];
const twoHoursAgo = -2;
for (const locale of locales) {
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
console.log(`${locale}: ${rtf.format(twoHoursAgo, 'hour')}`);
}
// en: 2 hours ago
// fr: il y a 2 heures
// de: vor 2 Stunden
// ja: 2 時間前
// ar: قبل ساعتين
Most date libraries ship locale data as separate imports or packages — kilobytes of JSON per language. Intl.RelativeTimeFormat uses the locale data already in the JavaScript engine. Nothing extra to load.
What it doesn't do
Intl.RelativeTimeFormat formats a pre-computed duration. It doesn't parse date strings, calculate diffs, or understand "last Monday." If you need date arithmetic — adding months, finding the start of a week, working with time zones — you still need a library or the upcoming Temporal API. But if you already have two timestamps and just want to display the difference as human text, Intl.RelativeTimeFormat is the right tool.
Browser support
Intl.RelativeTimeFormat is Baseline 2020: Chrome 71 (2018), Firefox 65 (2019), Safari 14 (2020), Node.js 12. It's been in every major environment for years. No polyfill needed for any currently-supported browser.
🎮 Try it yourself
▶️ Open the interactive playground →
Runs right in your browser — poke at it and watch the concept react live.
🧠 Test yourself
Think it clicked? Take the 9-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
The takeaway
Search your codebase for formatDistanceToNow, fromNow(), or any home-grown "X minutes ago" function. If the logic is "compute a diff, format it as human text," replace it with Intl.RelativeTimeFormat and a unit-picking helper. You get correct pluralization, locale-aware phrasing, and natural-language output like "yesterday" and "tomorrow" — with no added dependency and no locale bundle to manage.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
- 💼 LinkedIn — linkedin.com/in/parsa-jiravand
- ✉️ Email (work & contract inquiries): bestpractice2026@gmail.com
Top comments (0)