ToolPal
Numbers and mathematical symbols on a dark background

Number Formatting in JavaScript: A Complete Guide to Intl.NumberFormat

πŸ“· Antoine Dautry / Pexels

Number Formatting in JavaScript: A Complete Guide to Intl.NumberFormat

Master number formatting for international audiences. Learn how to use Intl.NumberFormat for currencies, percentages, and locale-aware number display in JavaScript.

April 10, 20269 min read

If you've ever built a web app for an international audience and then watched users report that numbers "look wrong," you've experienced the number formatting problem firsthand. I spent an embarrassing amount of time early in my career manually writing regex to insert commas into numbers β€” turns out JavaScript has had a perfectly good built-in solution for this since ES2015.

This guide covers everything you need to know about Intl.NumberFormat, including the gotchas I've personally run into, and points you to a practical online tool to experiment without writing any code.

The Problem: Not Everyone Uses the Same Number Format

Before we get into code, let's acknowledge the underlying issue. When you write 1234567.89 in code, how should it be displayed to a user?

  • United States / United Kingdom: 1,234,567.89 (comma as thousands separator, period as decimal)
  • Germany / Austria: 1.234.567,89 (period as thousands separator, comma as decimal)
  • France / Belgium: 1 234 567,89 (thin space as thousands separator, comma as decimal)
  • Switzerland: 1'234'567.89 (apostrophe as thousands separator)
  • India: 12,34,567.89 (uses a different grouping system β€” groups of 2 after the first group of 3)

Trying to handle all these cases with custom string manipulation is a recipe for bugs. The Intl.NumberFormat API handles all of this correctly, out of the box.

JavaScript's Intl.NumberFormat: An Overview

The Intl namespace contains several internationalization utilities. Intl.NumberFormat is the one you want for numbers. Browser support is excellent β€” it's available in every modern browser and Node.js 10+.

The basic constructor takes two arguments:

  1. A locale string (or array of locale strings)
  2. An options object
const formatter = new Intl.NumberFormat(locale, options);
const result = formatter.format(number);

Or more concisely for one-off use:

const result = new Intl.NumberFormat('en-US').format(1234567.89);
// "1,234,567.89"

You can also call Intl.NumberFormat.supportedLocalesOf() to check which locales are actually supported in the current environment β€” useful if you're targeting obscure regional locales.

Basic Usage

Let's start simple. Formatting a number with default options just applies the locale's standard decimal formatting:

// US English
new Intl.NumberFormat('en-US').format(1234567.89);
// "1,234,567.89"

// German
new Intl.NumberFormat('de-DE').format(1234567.89);
// "1.234.567,89"

// French
new Intl.NumberFormat('fr-FR').format(1234567.89);
// "1 234 567,89"

// Japanese
new Intl.NumberFormat('ja-JP').format(1234567.89);
// "1,234,567.89" (Japan uses the same separator convention as the US)

// Hindi (India)
new Intl.NumberFormat('hi-IN').format(1234567.89);
// "12,34,567.89"

Notice that Japan uses the same format as the US β€” Japanese numbers don't have the same comma/period confusion you see in European locales. The Indian grouping system, on the other hand, is genuinely different and would break any custom solution that assumes groups of 3.

Controlling Decimal Places

The minimumFractionDigits and maximumFractionDigits options give you control over how many decimal places appear:

// Always show 2 decimal places
new Intl.NumberFormat('en-US', {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
}).format(1234.5);
// "1,234.50"

// Show up to 3 decimal places, but no trailing zeros
new Intl.NumberFormat('en-US', {
  minimumFractionDigits: 0,
  maximumFractionDigits: 3
}).format(1234.5);
// "1,234.5"

Disabling Grouping

Sometimes you don't want the thousands separator β€” for example, when displaying a year or an ID number:

new Intl.NumberFormat('en-US', {
  useGrouping: false
}).format(2024);
// "2024" (not "2,024")

Currency Formatting

This is where Intl.NumberFormat really earns its keep. Currency formatting is surprisingly complex: you need the right symbol (or ISO code), placed in the right position, with the right number of decimal places for that specific currency.

// US Dollars
new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
}).format(1234.56);
// "$1,234.56"

// Euros (displayed for a German-speaking user)
new Intl.NumberFormat('de-DE', {
  style: 'currency',
  currency: 'EUR'
}).format(1234.56);
// "1.234,56 €"

// Japanese Yen
new Intl.NumberFormat('ja-JP', {
  style: 'currency',
  currency: 'JPY'
}).format(1234);
// "Β₯1,234" (no decimal places β€” yen doesn't have sub-units)

// British Pounds
new Intl.NumberFormat('en-GB', {
  style: 'currency',
  currency: 'GBP'
}).format(1234.56);
// "Β£1,234.56"

Gotcha #1: Currency and locale are independent. The currency option specifies the currency (ISO 4217 code), while the locale specifies the formatting style. You can display USD with German formatting:

new Intl.NumberFormat('de-DE', {
  style: 'currency',
  currency: 'USD'
}).format(1234.56);
// "1.234,56 $"

This is actually useful when you have a German user buying something priced in US dollars.

Gotcha #2: Currency display options. By default, you get the currency symbol. But you can ask for the ISO code or the full name:

const amount = 1234.56;

// Symbol (default)
new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  currencyDisplay: 'symbol'
}).format(amount);
// "$1,234.56"

// ISO code
new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  currencyDisplay: 'code'
}).format(amount);
// "USD 1,234.56"

// Full name
new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  currencyDisplay: 'name'
}).format(amount);
// "1,234.56 US dollars"

Gotcha #3: Some currencies have zero decimal places. Japanese Yen, Korean Won, and a handful of other currencies don't have sub-units. Intl.NumberFormat knows this and won't show decimal places when you use currency style with these currencies β€” even if you pass 1234.56. You get 1235 (rounded). This can be surprising if you're not expecting it.

Percentage Formatting

Displaying percentages correctly is trickier than it looks. The percent style multiplies your number by 100 and adds the percent sign:

// Note: pass a decimal fraction, not a whole number
new Intl.NumberFormat('en-US', {
  style: 'percent'
}).format(0.75);
// "75%"

new Intl.NumberFormat('en-US', {
  style: 'percent',
  minimumFractionDigits: 1,
  maximumFractionDigits: 1
}).format(0.7523);
// "75.2%"

// German percentage formatting
new Intl.NumberFormat('de-DE', {
  style: 'percent'
}).format(0.75);
// "75 %" (note the space before %)

The German locale includes a non-breaking space before the percent sign. If you're doing CSS comparisons or string matching, be aware of that.

The multiplication gotcha. Because the percent style multiplies by 100, if you already have a percentage as a whole number (like 75), you need to divide by 100 first:

const percentageValue = 75;
new Intl.NumberFormat('en-US', {
  style: 'percent'
}).format(percentageValue / 100);
// "75%"

Scientific Notation

For scientific or engineering applications, Intl.NumberFormat supports scientific notation:

new Intl.NumberFormat('en-US', {
  notation: 'scientific'
}).format(1234567890);
// "1.235E9"

new Intl.NumberFormat('en-US', {
  notation: 'engineering'
}).format(1234567890);
// "1.235E9" β€” engineering notation keeps exponents as multiples of 3

Honestly, I find myself using compact notation far more often than scientific notation in web apps.

Compact Notation

This is one of my favorite features. Compact notation formats large numbers as 1.2K, 3.5M, 2B, etc.:

new Intl.NumberFormat('en-US', {
  notation: 'compact'
}).format(1200);
// "1.2K"

new Intl.NumberFormat('en-US', {
  notation: 'compact'
}).format(3500000);
// "3.5M"

new Intl.NumberFormat('en-US', {
  notation: 'compact',
  compactDisplay: 'long'
}).format(1200);
// "1.2 thousand"

Compact notation is locale-aware too:

new Intl.NumberFormat('ja-JP', {
  notation: 'compact'
}).format(10000);
// "1δΈ‡" (Japanese uses δΈ‡ for 10,000)

new Intl.NumberFormat('zh-CN', {
  notation: 'compact'
}).format(10000);
// "1δΈ‡"

This is a really nice detail β€” in Japanese and Chinese, the natural grouping unit is 10,000 (δΈ‡) rather than 1,000, and Intl.NumberFormat respects that.

Common Gotchas and Edge Cases

NaN and Infinity. These are formatted as the locale's representation:

new Intl.NumberFormat('en-US').format(NaN);
// "NaN"

new Intl.NumberFormat('en-US').format(Infinity);
// "∞"

new Intl.NumberFormat('de-DE').format(Infinity);
// "∞" (same β€” infinity is locale-independent)

Very large numbers. JavaScript's number type has precision limits. For very large integers (beyond 2^53), you'll get precision loss. If you need exact large integer formatting, you should use BigInt:

new Intl.NumberFormat('en-US').format(9007199254740993n); // BigInt
// "9,007,199,254,740,993"

Yes, Intl.NumberFormat works with BigInt values.

Performance. Creating a new Intl.NumberFormat instance has some overhead. If you're formatting thousands of numbers in a loop, create one formatter instance and reuse it:

// Inefficient
numbers.forEach(n => new Intl.NumberFormat('en-US').format(n));

// Efficient
const formatter = new Intl.NumberFormat('en-US');
numbers.forEach(n => formatter.format(n));

The formatToParts method. Sometimes you need access to the individual parts of a formatted number (for styling purposes, for example, making the currency symbol a different color):

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
}).formatToParts(1234.56);
// [
//   { type: 'currency', value: '$' },
//   { type: 'integer', value: '1' },
//   { type: 'group', value: ',' },
//   { type: 'integer', value: '234' },
//   { type: 'decimal', value: '.' },
//   { type: 'fraction', value: '56' }
// ]

This is incredibly useful for React components where you want to style parts of a formatted number differently.

Using the Number Formatter Tool

If you want to experiment with different formatting options without writing code, the Number Formatter tool on ToolBox Hub lets you:

  1. Enter any number in the input field
  2. Select a locale from the dropdown (supports 30+ locales)
  3. Choose a style: decimal, currency, percent, or scientific
  4. For currency, select the currency code (USD, EUR, GBP, JPY, etc.)
  5. Set minimum and maximum decimal places
  6. Toggle grouping separators on or off
  7. See the formatted result instantly
  8. Copy the result or the JavaScript code to use in your project

It's particularly useful when you're unsure how a number will look in an unfamiliar locale β€” just try it before deploying.

When Intl.NumberFormat Isn't Enough

I want to be honest about the limitations here. Intl.NumberFormat is excellent, but it doesn't cover every scenario:

  • Accounting format (negative numbers in parentheses like (1,234.56)) isn't directly supported, though currencySign: 'accounting' gets you partway there
  • Custom format strings like #,##0.00 from Excel/spreadsheet software β€” you'd need a library like numeral.js or accounting.js for that
  • Number parsing (the reverse operation) β€” there's no Intl.NumberParse yet, which is a real gap. Parsing localized numbers back to JavaScript numbers is surprisingly hard

For most web app use cases though, Intl.NumberFormat covers you well.

If you're working with numbers in your web app, you might also find these tools useful:

Conclusion

Intl.NumberFormat is one of those APIs that seems simple on the surface but has real depth once you dig in. The key things to remember:

  • Always use it instead of manual formatting for user-facing numbers
  • Currency and locale are independent options β€” you can display any currency with any locale's formatting
  • The percent style multiplies by 100, so pass decimal fractions
  • notation: 'compact' is great for dashboards and charts
  • For performance, create one instance and reuse it
  • formatToParts() unlocks custom styling of number components

Numbers are everywhere in web apps β€” prices, statistics, measurements, counts. Getting them right for your users' locale is a small detail that makes a big difference in how professional your app feels.

Frequently Asked Questions

Share this article

XLinkedIn

Related Posts