How to Use Intl.DurationFormat

How to Use Intl.DurationFormat

Use Intl.DurationFormat when your app needs labels like 1 hr, 4 min, 1:04:09, or localized equivalents. Keep the duration math in your code, pass a plain duration object to new Intl.DurationFormat(locale, options).format(duration), and stop maintaining English-only string helpers for timers, dashboards, media lengths, and elapsed-time UI.

Here is the kind of helper that quietly spreads through a codebase:

function formatDuration(totalSeconds) {
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;

  return `${minutes} min ${seconds} sec`;
}

It works until the app needs another language, a clock-style label, hours, zero-padding, milliseconds, or different copy for a dashboard badge. Then the helper grows flags, and the flags grow edge cases.

Intl.DurationFormat does not calculate the duration for you. It formats structured duration data. That separation is exactly what makes it useful.

Start with a duration object

The basic shape is simple:

const duration = {
  hours: 1,
  minutes: 46,
  seconds: 40,
};

const formatter = new Intl.DurationFormat("en", {
  style: "short",
});

console.log(formatter.format(duration));

The exact string is locale-dependent. The important shift is that your app passes data, not a half-finished English sentence.

The duration object can use these fields:

  • years
  • months
  • weeks
  • days
  • hours
  • minutes
  • seconds
  • milliseconds
  • microseconds
  • nanoseconds

Use only the fields your UI needs. A job runtime might use hours, minutes, and seconds. A course-length badge might use hours and minutes. A benchmark panel might use seconds and milliseconds.

Convert seconds first, format second

Most app data starts as seconds or milliseconds. Convert that raw number into a duration object before formatting it:

function secondsToDuration(totalSeconds) {
  const safeSeconds = Math.max(0, Math.trunc(totalSeconds));

  const hours = Math.floor(safeSeconds / 3600);
  const minutes = Math.floor((safeSeconds % 3600) / 60);
  const seconds = safeSeconds % 60;

  return { hours, minutes, seconds };
}

function formatElapsedSeconds(totalSeconds, locale = "en") {
  const formatter = new Intl.DurationFormat(locale, {
    style: "short",
  });

  return formatter.format(secondsToDuration(totalSeconds));
}

console.log(formatElapsedSeconds(3805, "en"));
console.log(formatElapsedSeconds(3805, "fr-FR"));

That split keeps the code honest. Duration math is business logic. Duration wording is localization logic. Mixing them is how tiny utilities turn into awkward shared files with hidden assumptions. Even a small JavaScript helper can deserve more care than it first appears to, as anyone who has chased variations of checking for an empty string in JavaScript has probably seen.

Choose the right style

Intl.DurationFormat has four top-level style values:

new Intl.DurationFormat("en", { style: "long" });
new Intl.DurationFormat("en", { style: "short" });
new Intl.DurationFormat("en", { style: "narrow" });
new Intl.DurationFormat("en", { style: "digital" });

Use them by surface:

  • long for prose and help text.
  • short for cards, tables, dashboard labels, and status rows.
  • narrow for tight UI.
  • digital for clock-like output.

A media player usually wants digital output:

const mediaDuration = new Intl.DurationFormat("en", {
  style: "digital",
});

console.log(mediaDuration.format({ hours: 1, minutes: 4, seconds: 9 }));

A queue estimate or deployment timer often reads better as compact text:

const queueDuration = new Intl.DurationFormat("en", {
  style: "short",
});

console.log(queueDuration.format({ minutes: 8, seconds: 30 }));

For dashboard labels, the format choice affects scan speed. A chart subtitle that says a build averaged 2 min, 14 sec is usually easier to read than a custom string with inconsistent abbreviations. The same principle applies when you create a pie chart with Chart.js: the chart is only as useful as the labels around it.

Stabilize timer output with display options

Sometimes the default behavior is too flexible. A timer may need visible zero units so the layout does not jump.

Use the per-unit display options:

const timerFormatter = new Intl.DurationFormat("en", {
  style: "digital",
  hoursDisplay: "always",
  minutesDisplay: "always",
  secondsDisplay: "always",
});

console.log(timerFormatter.format({ hours: 0, minutes: 3, seconds: 7 }));

Display options use "always" or "auto". Use "always" for stable timer surfaces. Use "auto" when empty units would add noise.

Use fractionalDigits only where precision helps

For sub-second UI, fractionalDigits controls decimal precision. The valid range is 0 through 9.

const benchmarkFormatter = new Intl.DurationFormat("en", {
  style: "digital",
  seconds: "numeric",
  milliseconds: "numeric",
  fractionalDigits: 3,
});

console.log(
  benchmarkFormatter.format({
    seconds: 2,
    milliseconds: 345,
  })
);

This belongs in benchmark summaries, request timing, animation debug panels, and test output. Most product UI should round more aggressively. Precision is only useful when the reader can act on it.

Reach for formatToParts when markup matters

If you need to style numbers and units separately, use formatToParts(duration) instead of splitting a formatted string.

const formatter = new Intl.DurationFormat("en", {
  style: "short",
});

const parts = formatter.formatToParts({
  hours: 2,
  minutes: 30,
});

console.log(parts);

Each part has a type, a value, and sometimes a unit. A simple renderer can keep the localized order while styling integer tokens:

function renderDurationParts(duration, locale = "en") {
  const formatter = new Intl.DurationFormat(locale, {
    style: "short",
  });

  return formatter
    .formatToParts(duration)
    .map((part) => {
      if (part.type === "integer") {
        return `<strong>${part.value}</strong>`;
      }

      return part.value;
    })
    .join("");
}

In a real app, return framework nodes instead of an HTML string. The key rule is the same: keep the formatter in charge of order and separators.

Check support before removing fallbacks

Intl.DurationFormat became Baseline Newly available on March 4, 2025, so current major browser engines support it. That does not mean every environment your app supports has it.

Feature-detect it:

function hasDurationFormat() {
  return "DurationFormat" in Intl;
}

If you support older browsers, embedded WebViews, older Electron shells, or older server-side runtimes, keep a fallback or use a polyfill.

FormatJS provides @formatjs/intl-durationformat:

npm i @formatjs/intl-durationformat

For a global polyfill:

import "@formatjs/intl-durationformat/polyfill.js";

For TypeScript-friendly usage, FormatJS also documents an ES module import:

import { DurationFormat } from "@formatjs/intl-durationformat";

const formatter = new DurationFormat("en", {
  style: "long",
});

Read the package docs before adding it to a legacy bundle because the polyfill has its own runtime requirements.

Test with more than English

Do not ship after checking only en. Try a short list of locales that force different output shapes:

const locales = ["en", "fr-FR", "de-DE", "ja-JP"];
const duration = { hours: 1, minutes: 5, seconds: 9 };

for (const locale of locales) {
  const formatter = new Intl.DurationFormat(locale, {
    style: "short",
  });

  console.log(locale, formatter.format(duration));
}

If users can choose a locale, Intl.DurationFormat.supportedLocalesOf(locales) can tell you which requested locales are supported for duration formatting without falling back to the runtime default:

const supported = Intl.DurationFormat.supportedLocalesOf([
  "en",
  "fr-FR",
  "de-DE",
]);

console.log(supported);

Also check layout. Localized strings can be longer than English. A badge, chart label, table cell, or button that fits 1 hr, 5 min may wrap in another locale. If you already test browser location and timezone behavior, add locale checks to that same pass; it is close to the workflow for changing browser location and timezone for testing, but the thing under review is copy shape instead of time calculation.

Good first places to use it

Good targets:

  • Media duration labels
  • Upload or export progress estimates
  • Background job runtimes
  • Dashboard KPI subtitles
  • Workout, lesson, or course lengths
  • Retry cooldowns
  • Benchmark and test summaries

Poor targets:

  • Billing periods with legal wording
  • Calendar ranges like "January 1 to January 5"
  • Relative copy like "3 minutes ago"
  • Fuzzy product language like "less than a minute"

For relative copy, use a relative-time formatter. For date ranges, use date formatting. For fuzzy language, write the business rules first and format only the exact pieces that remain.

Replacement checklist

Use this order when replacing a custom duration helper:

  1. Find helpers returning strings like hr, min, sec, ms, or clock-style output.
  2. Separate duration math from duration formatting.
  3. Add a small secondsToDuration() or millisecondsToDuration() converter.
  4. Use Intl.DurationFormat for the final label.
  5. Pick long, short, narrow, or digital by UI surface.
  6. Add feature detection or a polyfill if your supported runtimes need it.
  7. Test English plus a few non-English locales.
  8. Check layout at narrow widths.

The win is not just fewer lines of code. It is fewer hidden assumptions. Your app still owns the duration calculation, but the display layer stops pretending English string glue is a formatting system.

Sources

Walt is a software engineer, startup founder and previous mentor for a coding bootcamp. He has been creating software for the past 20+ years.
No comments posted yet
// Add a comment
// Color Theme

Custom accent
Pick any color
for the accent