This relative time calculator turns any timestamp into a human-readable phrase like "3 days ago" or "in 2 hours", the same style used in GitHub issues, Slack messages, and most modern UIs. Paste a Unix epoch (seconds, milliseconds, or microseconds), an ISO 8601 string, an RFC 2822 date, or virtually any date format your browser can parse — the tool auto-detects the format, shows the picked unit, breaks the delta down across seconds/minutes/hours/days/weeks/months/years, and displays both local and UTC times. A bulk mode converts whole lists at once, and the live clock keeps sub-minute phrases accurate.
Every successful product UI shows times relatively rather than absolutely. GitHub commits, Slack messages, Twitter posts, Stripe transactions, Linear issues — they all tell you "5 minutes ago" or "yesterday" rather than dumping a full ISO timestamp. Relative formatting feels human; absolute formatting feels like log output. This guide covers the threshold buckets that produce natural-sounding phrases, the Intl.RelativeTimeFormat API that ships in every modern browser, the i18n surprises (English uses "yesterday"; Japanese has separate "昨日" for spoken vs written), and the live-update pattern that keeps "1 minute ago" from going stale.
The convention used by Twitter, GitHub, Slack, and the JavaScript Intl spec all converge on roughly these thresholds:
| Delta | Phrase | Notes |
|---|---|---|
| 0–10 s | "just now" | Sub-resolution; treats "right now" as a single bucket. |
| 10–60 s | "42 seconds ago" | Show seconds for live feeds; some products skip and jump to "less than a minute ago". |
| 1–60 min | "5 minutes ago" | The most common bucket for UI activity feeds. |
| 1–24 h | "3 hours ago" | Round down (3.7 h shows as "3 hours ago", not "4"). |
| 1–7 days | "yesterday" / "3 days ago" | "Yesterday" is special-cased. |
| 1–4 weeks | "2 weeks ago" | Some UIs prefer "10 days ago" up to ~14 days, then switch. |
| 1–12 months | "6 months ago" | Calendar months, not 30-day blocks. |
| ≥ 1 year | "3 years ago" or absolute date | GitHub switches to absolute "Mar 2023" past 1 year. |
The browser ships an i18n-aware relative-time formatter as of 2018 (Chrome 71, Firefox 65, Safari 14). It handles plurals, future vs past, and locale-specific phrasing automatically:
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-1, 'day'); // "yesterday"
rtf.format( 0, 'day'); // "today"
rtf.format( 1, 'day'); // "tomorrow"
rtf.format(-3, 'day'); // "3 days ago"
rtf.format( 5, 'minute'); // "in 5 minutes"
// numeric: 'always' forces numbers (for log displays)
new Intl.RelativeTimeFormat('en', { numeric: 'always' })
.format(-1, 'day'); // "1 day ago"
// Different locales handle phrasing differently
new Intl.RelativeTimeFormat('ja', { numeric: 'auto' })
.format(-1, 'day'); // "昨日"
new Intl.RelativeTimeFormat('de', { numeric: 'auto' })
.format(-1, 'day'); // "gestern"
This is the recommended approach in 2026. It eliminates the "I shipped my homegrown relative formatter and it broke for German users" class of bug.
If the user lands on the page and a comment was posted 30 seconds ago, the UI shows "30 seconds ago". One minute later it should say "1 minute ago", then "2 minutes ago", and so on. The standard implementation:
function startRelativeTimeUpdates() {
const elements = document.querySelectorAll('time[datetime]');
function updateAll() {
const now = Date.now();
elements.forEach(el => {
const t = new Date(el.dateTime).getTime();
el.textContent = formatRelative(t - now);
});
}
updateAll();
setInterval(updateAll, 30_000); // refresh every 30s
}
For a feed with hundreds of timestamps, batching updates like this beats a per-element timer. Stop the interval when the tab is hidden (use the Page Visibility API) to save battery.
Future and past tense in relative time should never collide. A naive implementation might show:
30 seconds ago — the comment was posted just now (correct).30 seconds ago — your access token expired 30 seconds ago (also correct, but the phrase is identical).in 30 seconds — your access token expires in 30 seconds (different).Always preserve the sign of the delta. "Created 5 min ago" and "Expires in 5 min" are different states; collapsing them to the same phrase has caused real bugs in dashboard expiry warnings.
"Yesterday" is a calendar concept, not a duration. A timestamp 18 hours ago might be "yesterday" or "today" depending on the user's local time:
Use the local-date concept (zero-out the time portion of both timestamps and compare days) when you want to special-case "today", "yesterday", "last week".
| Locale | "3 days ago" | "in 3 days" |
|---|---|---|
| English (en) | 3 days ago | in 3 days |
| German (de) | vor 3 Tagen | in 3 Tagen |
| Spanish (es) | hace 3 días | dentro de 3 días |
| French (fr) | il y a 3 jours | dans 3 jours |
| Japanese (ja) | 3 日前 | 3 日後 |
| Arabic (ar) | قبل 3 أيام | خلال 3 أيام |
| Russian (ru) | 3 дня назад | через 3 дня |
Languages with grammatical gender (Russian, Arabic) or different plural rules (Russian: 1 / 2-4 / 5+ all use different forms) make hand-rolling impossible. Always use Intl.RelativeTimeFormat for any product going beyond English.
| Tool | Bundle size | Best for |
|---|---|---|
| Intl.RelativeTimeFormat | 0 (built-in) | Everything new in 2026. |
date-fns formatDistanceToNow | ~14 KB minified+gzipped | Apps already using date-fns; tree-shakable. |
day.js + relativeTime plugin | ~3 KB | Lightweight Moment.js replacement. |
luxon toRelative() | ~70 KB | Heavy-duty calendars and TZ work. |
| Moment.js fromNow() | ~232 KB | Legacy code only — Moment is in maintenance mode. |
Vercel @formatjs/intl-relativetimeformat | ~6 KB + locale files | Polyfill for older browsers (still useful for India/Africa Android fleets). |
title or <time datetime> attribute so it's accessible.<time datetime="2026-05-02T14:30:00Z">3 hours ago</time> so assistive tech can announce the exact value if needed.2024-01-15T12:00:00Z and offset variants), RFC 2822 (Mon, 15 Jan 2024 12:00:00 GMT), and any natural string JavaScript's Date constructor accepts. Negative integers represent pre-1970 epochs. The detected format is shown just above the result.invalid marker rather than aborting the whole batch, so you can copy-paste messy data without pre-cleaning it.setInterval so sub-minute phrases stay accurate in real time. Once the delta exceeds a minute, the visible phrase only changes when the unit threshold is crossed (e.g., from "59 minutes" to "1 hour"), but the internal calculation keeps ticking.1710000000) are standard in Unix, Go, and most databases. Milliseconds (13 digits, e.g., 1710000000000) are native to JavaScript (Date.now()) and Java. Microseconds (16 digits) occasionally appear in Postgres and Python time.time_ns(). This tool detects which one you pasted based on the digit count and parses accordingly.All tools run in your browser, no signup required, nothing sent to a server.