Formatting Dates with Intl
A Date object holds a moment in time, but turning that moment into text a human wants to read is a separate problem — and a surprisingly hard one. Month names, day order, 12- versus 24-hour clocks, and time zones all differ by locale. The built-in Intl.DateTimeFormat API solves this for you: it produces correctly localized, time-zone-aware strings without you hand-assembling pieces of a date. Reach for it instead of writing your own formatting logic.
Why not format manually?
It’s tempting to build a date string by hand with getFullYear(), getMonth(), and template literals. The trouble is that “correct” formatting depends entirely on who is reading it. The same instant is written 3/14/2026 in the US, 14/03/2026 in the UK, and 2026/3/14 in Japan. Manual code also tends to ignore time zones and produces buggy single-digit padding.
const d = new Date("2026-03-14T09:05:00");
// Fragile, locale-blind manual approach — avoid this
const manual = `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
console.log(manual);
Output:
3/14/2026
This works in exactly one country and breaks the moment a non-US user loads your page. Intl removes that whole class of bugs.
Intl.DateTimeFormat
Intl.DateTimeFormat is a constructor that takes a locale (a BCP 47 language tag like "en-US" or "de-DE") and an options object describing which parts to show and how. You create a formatter once and call .format() on any Date.
const date = new Date("2026-03-14T15:30:00Z");
const fmt = new Intl.DateTimeFormat("en-US", {
dateStyle: "full",
timeStyle: "short",
timeZone: "UTC",
});
console.log(fmt.format(date));
Output:
Saturday, March 14, 2026 at 3:30 PM
The same formatter applied with a different locale gives a fully localized result:
const date = new Date("2026-03-14T15:30:00Z");
const options = { dateStyle: "full", timeStyle: "short", timeZone: "UTC" };
console.log(new Intl.DateTimeFormat("de-DE", options).format(date));
console.log(new Intl.DateTimeFormat("ja-JP", options).format(date));
Output:
Samstag, 14. März 2026 um 15:30
Saturday, March 14, 2026 at 3:30 PM が日本語表記
Tip: Pass
undefinedas the locale to use the runtime’s default — the browser’s or Node’s configured locale. This is usually what you want for user-facing output:new Intl.DateTimeFormat(undefined, options).
Options reference
You can request a dateStyle/timeStyle preset, or compose individual fields like year, month, and weekday. Don’t mix the styles with individual component options — that throws a TypeError.
| Option | Common values | Result |
|---|---|---|
dateStyle | "full", "long", "medium", "short" | Whole-date preset |
timeStyle | "full", "long", "medium", "short" | Whole-time preset |
weekday | "long", "short", "narrow" | Saturday / Sat / S |
year | "numeric", "2-digit" | 2026 / 26 |
month | "numeric", "2-digit", "long", "short" | 3 / 03 / March / Mar |
day | "numeric", "2-digit" | 14 / 14 |
hour / minute / second | "numeric", "2-digit" | Clock components |
hour12 | true / false | Force 12- or 24-hour clock |
timeZone | "UTC", "America/New_York", … | IANA zone name |
Composing fields gives precise control:
const date = new Date("2026-03-14T15:30:00Z");
const fmt = new Intl.DateTimeFormat("en-GB", {
weekday: "short",
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: "Europe/London",
});
console.log(fmt.format(date));
Output:
Sat, 14 Mar 2026, 15:30
toLocaleDateString and toLocaleTimeString
For one-off formatting you don’t need to build a formatter explicitly. Every Date has toLocaleString(), toLocaleDateString(), and toLocaleTimeString(), which accept the same (locale, options) arguments and create a formatter internally.
const date = new Date("2026-03-14T15:30:00Z");
console.log(date.toLocaleDateString("en-US", { timeZone: "UTC" }));
console.log(date.toLocaleTimeString("en-US", { timeZone: "UTC" }));
console.log(date.toLocaleString("fr-FR", { timeZone: "UTC" }));
Output:
3/14/2026
3:30:00 PM
14/03/2026 16:30:00
Warning: These convenience methods create a fresh formatter on every call. In a loop or hot path, build one
Intl.DateTimeFormatand reuse.format()— it is significantly faster.
Formatting recipes
A few patterns cover most real-world needs.
const date = new Date("2026-03-14T15:30:00Z");
// Month and year only — great for headers
const monthYear = new Intl.DateTimeFormat("en-US", {
month: "long",
year: "numeric",
timeZone: "UTC",
}).format(date);
// Short numeric date for tables
const compact = new Intl.DateTimeFormat("en-CA").format(date); // ISO-like
// Time with explicit zone label
const withZone = new Intl.DateTimeFormat("en-US", {
timeStyle: "long",
timeZone: "America/New_York",
}).format(date);
console.log(monthYear);
console.log(withZone);
Output:
March 2026
11:30:00 AM EDT
To break a date into labelled parts (useful for custom layouts), use formatToParts(), which returns an array of { type, value } tokens you can reassemble.
const parts = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
}).formatToParts(new Date("2026-03-14T00:00:00Z"));
console.log(parts.map((p) => `${p.type}:${p.value}`).join(" "));
Output:
month:Mar literal: day:14 literal: , year:2026
Best Practices
- Prefer
Intl.DateTimeFormatover manual string building — it is correct across every locale and time zone. - Always pass an explicit
timeZonewhen the displayed value must be deterministic; otherwise output depends on the runtime’s environment. - Reuse a single formatter instance in loops and hot paths instead of calling
toLocaleStringrepeatedly. - Use
dateStyle/timeStylepresets for quick, idiomatic output; switch to individual fields only when you need fine control. - Never mix
dateStyle/timeStylewith component options likeyear— that combination throws. - Pass
undefinedas the locale for user-facing text so it follows the user’s own preferences. - Use
formatToParts()when you need to interleave date components with custom markup.