Count words in JavaScript

[...new Intl.Segmenter('en', { granularity: 'word' }).segment(text)].filter((s) => s.isWordLike).length

[...new Intl.Segmenter('en', { granularity: 'word' }).segment(text)].filter((s) => s.isWordLike).length counts words by Unicode’s word rules (UAX #29). The segmenter cuts the text into words, spaces and punctuation, and isWordLike is true only for the words. It’s built into Node and every current browser, with no package to install.

const segmenter = new Intl.Segmenter('en', { granularity: 'word' });
const text = "We don't ship on Fridays — see you Monday 👍";
const words = [...segmenter.segment(text)].filter((s) => s.isWordLike).map((s) => s.segment);
// ["We", "don't", "ship", "on", "Fridays", "see", "you", "Monday"]
words.length;
// 8

That’s Node 22. The contraction stays whole, and the dash and the emoji aren’t words. The counter below gives the same 8.

The split(/\s+/) trap

text.split(/\s+/).length;          // 10: the dash and the emoji count
''.split(/\s+/).length;            // 1, for no words at all
' hello world '.split(/\s+/);      // ['', 'hello', 'world', '']: 4

split returns [''] for an empty string, and a leading or trailing space adds an empty string at that end. If space-separated runs are what you want, as wc -w counts them, match them instead of splitting: (text.match(/\S+/g) ?? []).length is 0 for an empty string and 10 here.

Text without spaces

Chinese, Japanese and Thai don’t put spaces between words, so split sees a whole run of text as one word. The segmenter splits them with ICU’s dictionaries, even with the locale set to en:

[...segmenter.segment('我吃苹果。今天天气很好。')].filter((s) => s.isWordLike).map((s) => s.segment);
// ["我", "吃", "苹果", "今天", "天气", "很好"]
[...segmenter.segment('ผมกินข้าว')].filter((s) => s.isWordLike).length;
// 3

The tool counts 6 and 3 as well. Dictionary splits come from the ICU version that ships with the browser or Node; this is ICU 78.2.

Where the tool goes further

The segmenter keeps 1,000 and 3.5 whole, but splits a hyphenated compound, a time and a URL:

[...segmenter.segment('The follow-up is at 9:00, see https://example.com/notes?v=2')].filter((s) => s.isWordLike).length;
// 13: "follow", "up", "9", "00", "https", "example.com", "notes", "v", "2" …

The tool counts 7: it rejoins words across a hyphen and digits across a colon, and treats each URL and email address as one word.

Characters, while you’re at it

'👍🏽'.length is 4, because length counts UTF-16 code units. [...'👍🏽'].length counts code points, 2. A grapheme segmenter counts what a reader sees:

[...new Intl.Segmenter('en', { granularity: 'grapheme' }).segment('👍🏽')].length;
// 1

That’s how the tool’s Characters count works, so a flag such as 🇵🇱 is 1 character too.

Try it in the Word Counter

  • 8Words
  • 43Characters
  • 34Without spaces
  • 1Sentences
  • 1Paragraphs
  • 1Lines
  • 4Avg word length

2 s to read 3 s to say aloud

Estimated at 238 words a minute reading and 150 speaking.

Most used words

WordCountShare of all words
don't112.5%
fridays112.5%
monday112.5%
see112.5%
ship112.5%

Runs in your browser. Nothing you enter is sent anywhere.