Showing Human-Readable Time in the Browser
Your server stores timestamps in UTC. Your users live in dozens of timezones. The gap between these two realities—raw ISO strings versus “2 hours ago”—determines whether your interface feels native or foreign.
Modern browsers now handle human-readable time in JavaScript without third-party libraries. This article covers the native APIs you should reach for: Intl.DateTimeFormat, Intl.RelativeTimeFormat, Intl.DurationFormat, and the Temporal API for timezone-aware logic.
Key Takeaways
- Use
Intl.DateTimeFormatfor locale-aware absolute timestamps, passing IANA timezone identifiers directly to avoid manual offset math. - Use
Intl.RelativeTimeFormatwith a small helper function to produce natural phrases like “yesterday” or “3 hours ago.” - Use
Intl.DurationFormatfor elapsed time and countdowns, but check browser support since baseline availability arrived only in 2025. - The Temporal API replaces the error-prone
Dateobject with explicit, immutable types—adopt it for new projects while relying on Intl formatters for existing codebases.
Absolute Timestamps with Intl.DateTimeFormat
When users need exact dates and times, Intl.DateTimeFormat handles localization automatically. It respects the user’s locale and formats dates according to regional conventions.
const date = new Date('2026-03-15T14:30:00Z')
const formatter = new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'America/New_York'
})
console.log(formatter.format(date)) // "Mar 15, 2026, 10:30 AM"
The API accepts IANA timezone identifiers directly, eliminating manual offset calculations. For user-local formatting, detect the timezone automatically:
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
This returns values like "Europe/London" or "Asia/Tokyo", which you can pass directly to formatters.
Relative Time with Intl.RelativeTimeFormat
Social feeds, notifications, and activity logs benefit from relative timestamps. Intl.RelativeTimeFormat produces phrases like “3 days ago” or “in 2 hours” with proper localization.
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
rtf.format(-1, 'day') // "yesterday"
rtf.format(-3, 'hour') // "3 hours ago"
rtf.format(2, 'week') // "in 2 weeks"
The numeric: 'auto' option produces natural language (“yesterday”) instead of literal numbers (“1 day ago”) when appropriate.
You need to calculate the difference yourself—the API formats values, not dates. A simple helper works:
function getRelativeTime(date) {
const now = Date.now()
const diffInSeconds = Math.round((date - now) / 1000)
const units = [
{ unit: 'year', seconds: 31536000 },
{ unit: 'month', seconds: 2592000 },
{ unit: 'day', seconds: 86400 },
{ unit: 'hour', seconds: 3600 },
{ unit: 'minute', seconds: 60 },
{ unit: 'second', seconds: 1 }
]
for (const { unit, seconds } of units) {
if (Math.abs(diffInSeconds) >= seconds) {
const value = Math.round(diffInSeconds / seconds)
return new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
.format(value, unit)
}
}
return 'just now'
}
The month and year values above use fixed second approximations. For calendar-accurate differences across varying month lengths or leap years, use Temporal or a dedicated date library instead of fixed second constants.
Note that Math.round can occasionally push a value into the next unit (for example, 89 seconds rounds to 1 when divided by 60, producing “1 minute ago” instead of “89 seconds ago”). If you need stricter boundaries, use Math.trunc instead.
Discover how at OpenReplay.com.
Duration Formatting with Intl.DurationFormat
For elapsed time, countdowns, or video lengths, Intl.DurationFormat provides consistent output across locales:
const duration = { hours: 2, minutes: 45, seconds: 30 }
const df = new Intl.DurationFormat('en', { style: 'long' })
console.log(df.format(duration)) // "2 hours, 45 minutes, 30 seconds"
const dfShort = new Intl.DurationFormat('en', { style: 'digital' })
console.log(dfShort.format(duration)) // "2:45:30"
This API reached cross-engine baseline support in 2025 and is available in modern browsers, though you should still confirm compatibility with your specific support matrix.
The Temporal API for Safer Date Handling
The Temporal API addresses long-standing issues with JavaScript’s Date object—mutability, timezone confusion, and parsing inconsistencies. Temporal is supported in modern Chromium and Firefox releases as of 2026, but feature detection remains essential because support is not yet universal across all browsers.
if (typeof Temporal !== 'undefined') {
const now = Temporal.Now.zonedDateTimeISO('America/Los_Angeles')
console.log(now.toString())
}
Temporal provides distinct types for different concepts: Temporal.PlainDate for calendar dates, Temporal.PlainTime for wall-clock time, and Temporal.ZonedDateTime for timezone-aware moments. This explicitness prevents the bugs that plague Date-based code.
For new projects, Temporal offers the cleanest foundation. For existing codebases, the Intl formatters work seamlessly with legacy Date objects.
Practical Recommendations
Store timestamps as UTC ISO 8601 strings. Format them client-side using the Intl APIs. Avoid parsing non-ISO date strings—they behave inconsistently across browsers.
Reuse formatter instances when formatting multiple timestamps. Creating a formatter once and calling format() repeatedly is significantly faster than recreating one on every call.
Use semantic HTML for accessibility:
<time datetime="2026-03-15T14:30:00Z">March 15, 2026</time>
Conclusion
The native platform now handles what once required Moment.js or similar libraries. Intl.DateTimeFormat covers absolute timestamps, Intl.RelativeTimeFormat covers relative phrases, Intl.DurationFormat covers elapsed time, and the Temporal API provides a sound foundation for timezone-aware date logic. For human-readable time in the browser, these built-in tools are all you need.
FAQs
No. The API only formats a numeric value and a unit into a localized string. You must compute the difference between two dates yourself and choose the appropriate unit before passing them to the format method. The helper function shown in this article is a common pattern for that calculation.
Temporal is available in modern Chromium and Firefox releases as of 2026, but not yet universally supported across all browsers. Always use feature detection before calling Temporal methods, and consider a polyfill if you need broad compatibility. For formatting alone, the Intl APIs already have wide support and work fine with the legacy Date object.
Constructing an Intl formatter involves resolving locale data, timezone rules, and formatting options, which carries a measurable cost. Creating one instance and calling its format method repeatedly is significantly faster than rebuilding the formatter on every invocation, especially when rendering long lists of timestamps.
By default, yes. If you omit the timeZone option, the formatter uses the runtime environment timezone, which in a browser matches the users system setting. You can retrieve this value explicitly via Intl.DateTimeFormat().resolvedOptions().timeZone and pass it to other formatters or send it to your server.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.