Intl.ListFormat API
Format arrays into locale-aware readable lists
Introduction
When displaying multiple items to users, developers often join arrays with commas and add "and" before the last item:
const users = ["Alice", "Bob", "Charlie"];
const message = users.slice(0, -1).join(", ") + ", and " + users[users.length - 1];
// "Alice, Bob, and Charlie"
This approach hardcodes English punctuation rules and breaks in other languages. Japanese uses different particles, German has different spacing rules, and Chinese uses different separators. The Intl.ListFormat API solves this by formatting lists according to each locale's conventions.
What Intl.ListFormat does
Intl.ListFormat converts arrays into human-readable lists that follow the grammatical and punctuation rules of any language. It handles three types of lists that appear across all languages:
- Conjunction lists use "and" to connect items ("A, B, and C")
- Disjunction lists use "or" to present alternatives ("A, B, or C")
- Unit lists format measurements without conjunctions ("5 ft, 2 in")
The API knows how each language formats these list types, from punctuation to word choice to spacing.
Basic usage
Create a formatter with a locale and options, then call format() with an array:
const formatter = new Intl.ListFormat("en", {
type: "conjunction",
style: "long"
});
const items = ["bread", "milk", "eggs"];
console.log(formatter.format(items));
// "bread, milk, and eggs"
The formatter handles arrays of any length, including edge cases:
formatter.format([]); // ""
formatter.format(["bread"]); // "bread"
formatter.format(["bread", "milk"]); // "bread and milk"
List types control conjunctions
The type option determines which conjunction appears in the formatted list.
Conjunction lists
Use type: "conjunction" for lists where all items apply together. This is the default type:
const formatter = new Intl.ListFormat("en", { type: "conjunction" });
console.log(formatter.format(["HTML", "CSS", "JavaScript"]));
// "HTML, CSS, and JavaScript"
Common uses include displaying selected items, listing features, and showing multiple values that all apply.
Disjunction lists
Use type: "disjunction" for lists presenting alternatives or choices:
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
console.log(formatter.format(["credit card", "debit card", "PayPal"]));
// "credit card, debit card, or PayPal"
This appears in option lists, error messages with multiple solutions, and any context where users choose one item.
Unit lists
Use type: "unit" for measurements and technical values that should appear without conjunctions:
const formatter = new Intl.ListFormat("en", { type: "unit" });
console.log(formatter.format(["5 feet", "2 inches"]));
// "5 feet, 2 inches"
Unit lists work for measurements, technical specifications, and compound values.
List styles control verbosity
The style option adjusts how verbose the formatting appears. Three styles exist: long, short, and narrow.
const items = ["Monday", "Wednesday", "Friday"];
const long = new Intl.ListFormat("en", { style: "long" });
console.log(long.format(items));
// "Monday, Wednesday, and Friday"
const short = new Intl.ListFormat("en", { style: "short" });
console.log(short.format(items));
// "Monday, Wednesday, and Friday"
const narrow = new Intl.ListFormat("en", { style: "narrow" });
console.log(narrow.format(items));
// "Monday, Wednesday, Friday"
In English, long and short produce identical output for most lists. The narrow style omits the conjunction. Other languages show more variation between styles, particularly for disjunction lists.
How different languages format lists
Each language has distinct list formatting rules. Intl.ListFormat handles these differences automatically.
English uses commas, spaces, and conjunctions:
const en = new Intl.ListFormat("en");
console.log(en.format(["Tokyo", "Paris", "London"]));
// "Tokyo, Paris, and London"
German uses the same comma structure but different conjunctions:
const de = new Intl.ListFormat("de");
console.log(de.format(["Tokyo", "Paris", "London"]));
// "Tokyo, Paris und London"
Japanese uses different separators and particles:
const ja = new Intl.ListFormat("ja");
console.log(ja.format(["東京", "パリ", "ロンドン"]));
// "東京、パリ、ロンドン"
Chinese uses different punctuation entirely:
const zh = new Intl.ListFormat("zh");
console.log(zh.format(["东京", "巴黎", "伦敦"]));
// "东京、巴黎和伦敦"
These differences extend beyond punctuation to spacing rules, conjunction placement, and grammatical particles. Hardcoding any single approach breaks for other languages.
Using formatToParts for custom rendering
The formatToParts() method returns an array of objects instead of a string. Each object represents one piece of the formatted list:
const formatter = new Intl.ListFormat("en");
const parts = formatter.formatToParts(["red", "green", "blue"]);
console.log(parts);
// [
// { type: "element", value: "red" },
// { type: "literal", value: ", " },
// { type: "element", value: "green" },
// { type: "literal", value: ", and " },
// { type: "element", value: "blue" }
// ]
Each part has a type and value. The type is either "element" for list items or "literal" for formatting punctuation and conjunctions.
This structure enables custom rendering where elements and literals need different styling:
const formatter = new Intl.ListFormat("en");
const items = ["Alice", "Bob", "Charlie"];
const html = formatter.formatToParts(items)
.map(part => {
if (part.type === "element") {
return `<strong>${part.value}</strong>`;
}
return part.value;
})
.join("");
console.log(html);
// "<strong>Alice</strong>, <strong>Bob</strong>, and <strong>Charlie</strong>"
This approach maintains locale-correct punctuation while applying custom presentation to the actual list items.
Reusing formatters for performance
Creating Intl.ListFormat instances has overhead. Create formatters once and reuse them:
// Create once
const listFormatter = new Intl.ListFormat("en", { type: "conjunction" });
// Reuse many times
function displayUsers(users) {
return listFormatter.format(users.map(u => u.name));
}
function displayTags(tags) {
return listFormatter.format(tags);
}
For applications with multiple locales, store formatters in a map:
const formatters = new Map();
function getListFormatter(locale, options) {
const key = `${locale}-${options.type}-${options.style}`;
if (!formatters.has(key)) {
formatters.set(key, new Intl.ListFormat(locale, options));
}
return formatters.get(key);
}
const formatter = getListFormatter("en", { type: "conjunction", style: "long" });
console.log(formatter.format(["a", "b", "c"]));
This pattern reduces repeated initialization costs while supporting multiple locales and configurations.
Formatting error messages
Form validation often produces multiple errors. Format them with disjunction lists to present options:
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
function validatePassword(password) {
const errors = [];
if (password.length < 8) {
errors.push("at least 8 characters");
}
if (!/[A-Z]/.test(password)) {
errors.push("an uppercase letter");
}
if (!/[0-9]/.test(password)) {
errors.push("a number");
}
if (errors.length > 0) {
return `Password must contain ${formatter.format(errors)}.`;
}
return null;
}
console.log(validatePassword("weak"));
// "Password must contain at least 8 characters, an uppercase letter, or a number."
The disjunction list clarifies that users need to fix any of these issues, and the formatting adapts to each locale's conventions.
Displaying selected items
When users select multiple items, format the selection with conjunction lists:
const formatter = new Intl.ListFormat("en", { type: "conjunction" });
function getSelectionMessage(selectedFiles) {
if (selectedFiles.length === 0) {
return "No files selected";
}
if (selectedFiles.length === 1) {
return `${selectedFiles[0]} selected`;
}
return `${formatter.format(selectedFiles)} selected`;
}
console.log(getSelectionMessage(["report.pdf", "data.csv", "notes.txt"]));
// "report.pdf, data.csv, and notes.txt selected"
This pattern works for file selections, filter choices, category selections, and any multi-select interface.
Handling long lists
For lists with many items, consider truncating before formatting:
const formatter = new Intl.ListFormat("en", { type: "conjunction" });
function formatUserList(users) {
if (users.length <= 3) {
return formatter.format(users);
}
const visible = users.slice(0, 2);
const remaining = users.length - 2;
return `${formatter.format(visible)}, and ${remaining} others`;
}
console.log(formatUserList(["Alice", "Bob", "Charlie", "David", "Eve"]));
// "Alice, Bob, and 3 others"
This maintains readability while indicating the total count. The exact threshold depends on your interface constraints.
Browser support and fallbacks
Intl.ListFormat works in all modern browsers since April 2021. Support includes Chrome 72+, Firefox 78+, Safari 14.1+, and Edge 79+.
Check support with feature detection:
if (typeof Intl.ListFormat !== "undefined") {
const formatter = new Intl.ListFormat("en");
return formatter.format(items);
} else {
// Fallback for older browsers
return items.join(", ");
}
For broader compatibility, use a polyfill like @formatjs/intl-listformat. Install it only for environments that need it:
if (typeof Intl.ListFormat === "undefined") {
await import("@formatjs/intl-listformat/polyfill");
}
Given current browser support, most applications can use Intl.ListFormat directly without polyfills.
Common mistakes to avoid
Creating new formatters repeatedly wastes resources:
// Inefficient
function display(items) {
return new Intl.ListFormat("en").format(items);
}
// Efficient
const formatter = new Intl.ListFormat("en");
function display(items) {
return formatter.format(items);
}
Using array.join() for user-facing text creates localization problems:
// Breaks in other languages
const text = items.join(", ");
// Works across languages
const formatter = new Intl.ListFormat(userLocale);
const text = formatter.format(items);
Assuming English conjunction rules apply universally leads to incorrect output in other locales. Always pass the user's locale to the constructor.
Not handling empty arrays can cause unexpected output:
// Defensive
function formatItems(items) {
if (items.length === 0) {
return "No items";
}
return formatter.format(items);
}
While format([]) returns an empty string, explicit empty state handling improves user experience.
When to use Intl.ListFormat
Use Intl.ListFormat whenever displaying multiple items in prose. This includes navigation breadcrumbs, selected filters, validation errors, user lists, category tags, and feature lists.
Do not use it for structured data displays like tables or option menus. Those components have their own formatting requirements outside of prose list rules.
The API replaces manual string concatenation and joining patterns. Any time you would write join(", ") for user-facing text, consider whether Intl.ListFormat provides better locale support.