DEV Community

ZahrionTech
ZahrionTech

Posted on

How to Build a Word Counter in JavaScript (Live Character, Word, and Reading-Time Counts)

I built a word counter last month while working on a small writing tool, and it turned out to be one of those deceptively simple features. Counting characters is trivial. Counting words correctly — handling double spaces, line breaks, tabs, pasted text from Word — takes a little more care than most tutorials show.

Here is the approach I settled on, with working code you can drop into any page.

The HTML

Nothing fancy: a textarea and a few stat placeholders.

<textarea id="text" rows="8" placeholder="Paste or type your text here..."></textarea>

<div id="stats">
  <p>Words: <span id="words">0</span></p>
  <p>Characters: <span id="chars">0</span></p>
  <p>Characters (no spaces): <span id="charsNoSpaces">0</span></p>
  <p>Sentences: <span id="sentences">0</span></p>
  <p>Reading time: <span id="readingTime">—</span></p>
</div>
Enter fullscreen mode Exit fullscreen mode

Counting words without the classic bug

The naive approach is text.split(' ').length. That breaks the moment someone types two spaces in a row, hits enter, or pastes content with tabs — every extra whitespace character gets counted as a word.

The fix is to split on any run of whitespace with a regex, then throw away empty strings:

function countWords(str) {
  const trimmed = str.trim();
  if (trimmed === '') return 0;
  return trimmed.split(/\s+/).length;
}
Enter fullscreen mode Exit fullscreen mode

\s+ matches spaces, tabs, newlines, and any combination of them, so "hello world\n\nfoo" correctly counts as 3 words instead of 6. The trim() handles the edge case where the text starts or ends with whitespace, which would otherwise produce a phantom empty entry.

Characters and sentences

Character counts are straightforward, but writers usually want two numbers: with spaces and without. Job applications and publishing platforms often ask for one or the other.

function countChars(str) {
  return str.length;
}

function countCharsNoSpaces(str) {
  return str.replace(/\s/g, '').length;
}
Enter fullscreen mode Exit fullscreen mode

Sentences are a heuristic — abbreviations like "Mr." will fool any simple splitter — but splitting on sentence-ending punctuation and dropping empties gets you close enough for a writing aid:

function countSentences(str) {
  if (str.trim() === '') return 0;
  return str
    .split(/[.!?]+/)
    .map(s => s.trim())
    .filter(s => s.length > 0)
    .length;
}
Enter fullscreen mode Exit fullscreen mode

Reading time

Most readability research lands around 200–250 words per minute for average adult readers. I use 200 as a conservative default:

function readingTime(wordCount, wpm = 200) {
  if (wordCount === 0) return '—';
  const minutes = wordCount / wpm;
  if (minutes < 1) return 'less than 1 min';
  return `${Math.ceil(minutes)} min`;
}
Enter fullscreen mode Exit fullscreen mode

Wiring it up live

Listen to the input event so counts update as the user types or pastes. For a textarea this small, running the counts directly in the handler is fine — no debounce needed unless you are also doing something expensive like syntax highlighting.

const textarea = document.getElementById('text');

textarea.addEventListener('input', () => {
  const value = textarea.value;
  const words = countWords(value);

  document.getElementById('words').textContent = words;
  document.getElementById('chars').textContent = countChars(value);
  document.getElementById('charsNoSpaces').textContent = countCharsNoSpaces(value);
  document.getElementById('sentences').textContent = countSentences(value);
  document.getElementById('readingTime').textContent = readingTime(words);
});
Enter fullscreen mode Exit fullscreen mode

One thing worth testing: paste a chunk of text copied from a PDF or a Word document. That is where naive counters fall apart (ligatures, non-breaking spaces, soft hyphens), and where the \s+ split earns its keep. If you want to go further, normalize the input first with str.normalize() and replace non-breaking spaces (\u00A0) with regular ones.

A note on performance: debouncing

For a plain textarea, running the counts on every input event is fine — these are microsecond operations on typical text. But if your counter lives inside a rich editor, or you are also running spellcheck, syntax highlighting, or autosave on each keystroke, wrap the handler in a debounce so it only fires after the user pauses:

function debounce(fn, ms = 150) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  };
}

textarea.addEventListener('input', debounce(updateCounts));
Enter fullscreen mode Exit fullscreen mode

150ms is the sweet spot: short enough that the numbers feel live, long enough to skip the work while someone is mid-word.

Handling messy pasted text

The real world pastes ugly text. Content copied from PDFs and Word documents often contains non-breaking spaces (\u00A0), which \s in JavaScript does match — good — but also soft hyphens (\u00AD) and zero-width characters that can glue words together or split them invisibly. If your counter needs to be robust, normalize first:

function normalize(str) {
  return str
    .normalize()                    // unify unicode forms
    .replace(/[-]/g, '') // strip zero-width chars
    .replace(/ /g, ' ');       // nbsp -> regular space
}
Enter fullscreen mode Exit fullscreen mode

Run normalize() on the value before counting, and your word count will survive the worst copy-paste jobs your users can throw at it.

When you don't want to build it

This is a fun 30-minute exercise, and I recommend it if you are learning DOM manipulation. But if you just need a quick count for an essay, a product description, or a meta description you are drafting, building your own is overkill — a free online word counter gives you words, characters, sentences, and reading time instantly, with nothing to install.

Top comments (0)