Number Formatting in JavaScript: toLocaleString() and Intl.NumberFormat

Quick Summary
- Call toLocaleString('en-US') on a number to add thousands separators, and always pass the locale so the result doesn't depend on the visitor's settings.
- Intl.NumberFormat takes the same options and handles currency, compact numbers like 1.2M, percentages, units and fixed decimals; create it once to format many numbers.
- toFixed() returns a string without separators and rounds the stored binary value, so (1.005).toFixed(2) gives 1.00; use it for machine-readable values, not display.
To format a number with commas as thousands separators in JavaScript, call toLocaleString() with a locale: (1234567.891).toLocaleString('en-US') returns 1,234,567.891. For currency, percentages, units, short forms like 1.2M or a fixed number of decimals, use the same built-in engine through Intl.NumberFormat. You don’t need a regular expression or a library.
Add thousands separators with toLocaleString()
const n = 1234567.891;
console.log(n.toLocaleString('en-US')); // 1,234,567.891
console.log(n.toLocaleString('de-DE')); // 1.234.567,891
console.log(n.toLocaleString('en-IN')); // 12,34,567.891The locale decides the separators and how digits are grouped. German swaps the comma and the dot. Indian English groups the last three digits of the whole part, then pairs of digits (lakhs and crores), which is why the same number reads 12,34,567.891.
Always pass the locale. Without it, toLocaleString() uses the default locale of the browser or server running the code, so the same line prints 1.234.567,891 for a visitor whose browser is set to German. That’s fine for text a visitor reads, but not for values you store, compare or send somewhere.
By default a plain number keeps up to three decimals, and longer fractions are rounded. The options below change that.
Reuse a formatter with Intl.NumberFormat
toLocaleString() passes its arguments to Intl.NumberFormat behind the scenes, and its second argument takes the same options object as the constructor. When you format many numbers with the same settings, for example every row in a table, create the formatter once and call its format() method. MDN recommends this because each toLocaleString() call has to look up the locale data again.
const formatter = new Intl.NumberFormat('en-US');
const totals = [1250, 98000.5, 1234567];
console.log(totals.map((total) => formatter.format(total)));
// [ '1,250', '98,000.5', '1,234,567' ]Control the decimal places
Two options set how many decimals you get:
minimumFractionDigitspads with zeros up to this many decimals.maximumFractionDigitsrounds to at most this many decimals.
const twoDecimals = new Intl.NumberFormat('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
console.log(twoDecimals.format(1234.5)); // 1,234.50
console.log(twoDecimals.format(3.14159)); // 3.14
const wholeNumbers = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 });
console.log(wholeNumbers.format(1234.5)); // 1,235A value exactly halfway rounds away from zero by default, so 1234.5 becomes 1,235. Keep the minimum at or below the maximum: if minimumFractionDigits ends up larger than maximumFractionDigits, the constructor throws a RangeError.
Format currency
Set style: 'currency' and an ISO 4217 code in currency. The locale decides where the symbol goes and which separators to use. The currency decides how many decimals are shown.
const amount = 1234567.891;
const usd = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
const eur = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' });
const jpy = new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' });
const inr = new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR' });
console.log(usd.format(amount)); // $1,234,567.89
console.log(eur.format(amount)); // 1.234.567,89 € (the space is U+00A0)
console.log(jpy.format(amount)); // ¥1,234,568
console.log(inr.format(amount)); // ₹12,34,567.89- The yen has no minor unit, so the amount is rounded to whole yen.
Intl.NumberFormatonly formats. It doesn’t convert between currencies, so the number must already be in the currency you name.currencyis required with the currency style. Leaving it out throws aTypeError.
A few more options are handy for prices:
const accounting = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
currencySign: 'accounting',
});
console.log(accounting.format(-1234.5)); // ($1,234.50)
const price = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
trailingZeroDisplay: 'stripIfInteger',
});
console.log(price.format(5)); // $5
console.log(price.format(5.5)); // $5.50currencySign: 'accounting' wraps negative amounts in parentheses. trailingZeroDisplay: 'stripIfInteger' drops the .00 from whole amounts but keeps both decimals otherwise. It’s a newer option (Chrome 106, Firefox 116, Safari 15.4). currencyDisplay switches the symbol for the code (USD 1,234,567.89) or the name (1,234,567.89 US dollars).
Compact numbers: 1.5K, 1.2M
For view counts, followers or file downloads, notation: 'compact' shortens large numbers:
const compact = new Intl.NumberFormat('en-US', { notation: 'compact' });
console.log(compact.format(1500)); // 1.5K
console.log(compact.format(1234567)); // 1.2M
console.log(compact.format(2500000000)); // 2.5B
const compactLong = new Intl.NumberFormat('en-US', {
notation: 'compact',
compactDisplay: 'long',
});
console.log(compactLong.format(1234567)); // 1.2 millionAdd maximumFractionDigits: 2 to get 1.23M instead. The abbreviations are localized too: German gives 1,2 Mio. for the same number, again with a no-break space.
Percentages and units
const percent = new Intl.NumberFormat('en-US', {
style: 'percent',
maximumFractionDigits: 1,
});
console.log(percent.format(0.256)); // 25.6%
const speed = new Intl.NumberFormat('en-US', {
style: 'unit',
unit: 'mile-per-hour',
});
console.log(speed.format(60)); // 60 mph
const fileSize = new Intl.NumberFormat('en-US', {
style: 'unit',
unit: 'megabyte',
unitDisplay: 'long',
maximumFractionDigits: 1,
});
console.log(fileSize.format(1.456)); // 1.5 megabytes- The percent style multiplies by 100, so pass
0.256for 25.6%, not25.6. unittakes one of the units on the supported list, such askilogram,liter,celsiusorgigabyte. Two units joined with-per-make a compound unit.unitDisplayisshortby default (1.5 MB).longspells the unit out, andnarrowis the shortest form (1.5MB).
toFixed() pitfalls
toFixed() is still the quick way to get a set number of decimals, but it isn’t a display formatter:
console.log((1.5).toFixed(2) + 1); // 1.501
console.log((1234.5).toFixed(2)); // 1234.50
console.log((1.005).toFixed(2)); // 1.00
console.log((1.005).toPrecision(21)); // 1.00499999999999989342
console.log((1e21).toFixed(2)); // 1e+21- It returns a string, so adding a number to the result joins them as text.
- It has no thousands separators and always uses a dot as the decimal point.
- It rounds the stored binary value, not the number you typed.
1.005is really stored as 1.00499999999999989342…, so it rounds down to1.00. In Node,Intl.NumberFormatwith two decimals prints1.01for the same value, so don’t expect the two to agree on halfway cases. - From
1e21up it switches to exponential notation. - Watch the minus sign:
-2.34.toFixed(1)is the number-2.3, not a string, because the method call runs before the minus. Write(-2.34).toFixed(1).
Use toFixed() for machine-readable strings, like a value you put in a hidden form field or send to an API. Use Intl.NumberFormat for anything people read.
Keep numbers as numbers until you display them
Format at the last step, right before the value goes on the page. A formatted string doesn’t convert back cleanly: Number('1,234.50') is NaN, and parseFloat('1,234.50') stops at the comma and returns 1. Keep the raw number for calculations, sorting and data attributes.
Also be careful when you compare formatted strings, for example in tests. Some locales use special spaces: French groups digits with a narrow no-break space (U+202F), and the German euro format puts a no-break space (U+00A0) before the €. They look like ordinary spaces, so a string typed with a normal space won’t match.
Related articles
- removing trailing zeros from Shopify prices with JavaScript: another price-formatting fix in the browser
- allowing only numbers in a text input: for the fields where users type amounts
