What You'll Build
A live word counter that analyzes text as you type. It counts words, characters, sentences, and paragraphs, estimates reading and speaking time, and shows the most frequently used keywords with their density percentages.
- Count words, characters, and characters without spaces
- Detect sentences and paragraphs automatically
- Calculate reading time (225 words/min) and speaking time (140 words/min)
- Show the top 12 keywords with frequency and density %
- Everything updates live as you type
📝 Word Counter
Start typing or paste your text below
🔑 Top Keywords
What You'll Learn
- String methods:
split(),replace(),trim(),toLowerCase() - Regular expressions:
\s,[.!?], and global flags — a gentle introduction - Arrays: filtering, mapping, and the
filter()method - Objects as frequency counters: the classic "count occurrences" pattern
- Sorting: ranking words by how often they appear
- Live analysis: processing text on every keystroke without lag
Why this project matters: Counting and ranking data is at the heart of search engines, recommendation systems, and analytics. This project teaches you the smallest version of that idea.
Build the HTML Structure
Create index.html. The layout is a textarea for input, a grid of stat cards, and a keywords list underneath.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Word Counter</title> </head> <body> <div class="card"> <h1>📝 Word Counter</h1> <p class="sub">Start typing or paste your text below</p> <textarea id="input" placeholder="Start typing..."></textarea> <div class="stats"> <div class="stat primary"> <div class="label">Words</div> <div class="val" id="words">0</div> </div> <div class="stat"> <div class="label">Characters</div> <div class="val" id="chars">0</div> </div> <div class="stat"> <div class="label">No Spaces</div> <div class="val" id="charsNo">0</div> </div> <div class="stat"> <div class="label">Sentences</div> <div class="val" id="sentences">0</div> </div> <div class="stat"> <div class="label">Paragraphs</div> <div class="val" id="paragraphs">0</div> </div> <div class="stat"> <div class="label">Reading</div> <div class="val" id="reading">0s</div> </div> <div class="stat"> <div class="label">Speaking</div> <div class="val" id="speaking">0s</div> </div> </div> <div class="keywords"> <h3>🔑 Top Keywords</h3> <div class="kw-list" id="kwList"> <span class="empty">Start typing to see keywords…</span> </div> </div> </div> <script src="script.js"></script> </body> </html>
Key structure point: The stat cards all share the same .stat class — the CSS handles the uniform look, and JavaScript fills each .val by its id. When you add new stats later, you just add a new card + a new ID.
Style It with CSS
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f1f5f9;
padding: 20px;
color: #0f172a;
}
.card {
background: #fff;
border-radius: 16px;
padding: 24px;
max-width: 640px;
margin: 0 auto;
box-shadow: 0 10px 40px rgba(15,23,42,0.08);
}
h1 { font-size: 1.3rem; margin-bottom: 4px; }
.sub { color: #64748b; font-size: 0.85rem; margin-bottom: 16px; }
textarea {
width: 100%;
min-height: 140px;
padding: 12px 14px;
border: 2px solid #e2e8f0;
border-radius: 10px;
font-size: 0.95rem;
font-family: inherit;
outline: none;
resize: vertical;
line-height: 1.6;
transition: 0.2s;
}
textarea:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}
/* Stats grid — auto-fits as many columns as fit */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(95px, 1fr));
gap: 8px;
margin-top: 16px;
}
.stat {
background: #f1f5f9;
border-radius: 10px;
padding: 12px 10px;
text-align: center;
}
.stat.primary { background: #eff6ff; }
.stat .label {
color: #64748b;
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 700;
margin-bottom: 4px;
}
.stat .val {
color: #0f172a;
font-size: 1.4rem;
font-weight: 900;
font-family: "Courier New", monospace;
line-height: 1;
}
.stat.primary .val { color: #1d4ed8; font-size: 1.6rem; }
/* Keywords */
.keywords { margin-top: 18px; }
.keywords h3 { font-size: 0.9rem; font-weight: 700; color: #0f172a; margin-bottom: 8px; }
.kw-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.kw {
background: #f1f5f9;
border-radius: 6px;
padding: 5px 9px;
font-size: 0.78rem;
color: #334155;
display: inline-flex;
align-items: center;
gap: 5px;
}
.kw .count { color: #1d4ed8; font-weight: 700; font-family: "Courier New", monospace; }
.kw .pct { color: #64748b; font-size: 0.7rem; }
.empty { color: #94a3b8; font-size: 0.85rem; font-style: italic; }
Write the JavaScript Logic
This is where the magic happens. The update() function runs on every keystroke and recalculates all the stats plus the keyword ranking.
// 1. Grab the textarea var input = document.getElementById("input"); // 2. Stop words to ignore when ranking keywords var stopWords = ["the","a","an","and","or","but","if","of","to","in","on","at","by","for","with","about","as","is","are","was","were","be","been","being","have","has","had","do","does","did","will","would","can","could","should","may","might","must","shall","it","its","this","that","these","those","i","you","he","she","we","they","them","his","her","our","your","their","my","me","us","him","so","not","no","yes","than","then","there","here","when","where","which","who","whom","what","how","why","all","any","some","each","every","both","few","more","most","other","such","only","own","same","too","very","just","also"]; // 3. The main update function — runs on every keystroke function update() { var text = input.value; // Split into words: trim, then split on any whitespace var wordArr = text.trim() ? text.trim().split(/\s+/) : []; var words = wordArr.length; // Characters var chars = text.length; var charsNo = text.replace(/\s/g, "").length; // Sentences: split on . ! ? and filter empties var sentences = text.trim() ? text.split(/[.!?]+(?:\s|$)/).filter(function(s) { return s.trim().length > 0; }).length : 0; // Paragraphs: split on blank lines var paragraphs = text.trim() ? text.split(/\n\s*\n/).filter(function(p) { return p.trim().length > 0; }).length : 0; // Time estimates (avg speeds) var reading = words > 0 ? Math.round((words / 225) * 60) : 0; var speaking = words > 0 ? Math.round((words / 140) * 60) : 0; // Write to the DOM document.getElementById("words").textContent = words; document.getElementById("chars").textContent = chars; document.getElementById("charsNo").textContent = charsNo; document.getElementById("sentences").textContent = sentences; document.getElementById("paragraphs").textContent = paragraphs; document.getElementById("reading").textContent = fmtTime(reading); document.getElementById("speaking").textContent = fmtTime(speaking); updateKeywords(wordArr, words); } // 4. Format seconds into "4m 30s" or "45s" function fmtTime(s) { if (s === 0) return "0s"; if (s < 60) return s + "s"; var m = Math.floor(s / 60); var sec = s % 60; return sec > 0 ? m + "m " + sec + "s" : m + "m"; } // 5. Count and rank keywords function updateKeywords(wordArr, totalWords) { var list = document.getElementById("kwList"); if (totalWords === 0) { list.innerHTML = '<span class="empty">Start typing to see keywords…</span>'; return; } // Frequency counter object var freq = {}; wordArr.forEach(function(w) { // Clean the word: lowercase, remove punctuation var clean = w.toLowerCase().replace(/[^a-z0-9']/g, ""); // Skip short words and stop words if (clean.length < 2 || stopWords.indexOf(clean) !== -1) return; freq[clean] = (freq[clean] || 0) + 1; }); // Convert object to array and sort by count descending var sorted = Object.keys(freq) .map(function(k) { return [k, freq[k]]; }) .sort(function(a, b) { return b[1] - a[1]; }) .slice(0, 12); if (sorted.length === 0) { list.innerHTML = '<span class="empty">Not enough words yet…</span>'; return; } // Render as chips list.innerHTML = sorted.map(function(pair) { var word = pair[0], count = pair[1]; var density = ((count / totalWords) * 100).toFixed(1); return '<span class="kw">' + word + ' <span class="count">' + count + '×</span> <span class="pct">' + density + '%</span></span>'; }).join(""); } // 6. Run on every keystroke input.addEventListener("input", update); // 7. Initial run update();
Save the file and open in your browser. Start typing in the textarea — every stat updates instantly, and the top keywords list re-ranks as you type.
Understand the Key Patterns
The frequency counter pattern
This is one of the most useful patterns in all of programming. Whenever you need to count how many times something appears in a list, you use the same approach:
var freq = {}; items.forEach(function(item) { freq[item] = (freq[item] || 0) + 1; });
The trick (freq[item] || 0) + 1 means: "if freq[item] doesn't exist yet, treat it as 0, then add 1." It's a one-line pattern that replaces a 3-line if-else. Learn it — you'll use it constantly.
The regex shorthand
In this project you used three regex patterns. Here's what they mean:
/\s+/— one or more whitespace characters (spaces, tabs, newlines). Used for splitting into words./\s/g— all whitespace characters (thegflag = global). Used to remove spaces./[.!?]+(?:\s|$)/— one or more sentence-ending punctuation marks, followed by a space or end of string. Used to split into sentences.
Beginner trap: Writing text.split(" ") (literal space) instead of text.split(/\s+/) (any whitespace). If the user has double spaces or tabs, the literal version counts empty strings as words and breaks the count.
Why use objects as counters instead of arrays
You could use an array of words and loop through it every time you want to count. But that's O(n²) — slow for large text. Using an object as a frequency map is O(n) — each word is visited exactly once.
For a 1,000-word article: the array approach does ~500,000 operations. The object approach does 1,000. Ten times faster on small text, thousands of times faster on large text.
Practice Challenges
🟢 Beginner
- Add a Lines stat — count of
text.split("\n").length - Add an Average word length stat — total letters ÷ word count
- Add a Clear button that empties the textarea
🟡 Intermediate
- Add a Target word count progress bar — user picks a goal (500, 1000, 2000) and the bar fills as they type
- Add a Unique words stat — how many distinct words appear
- Add UPPER / lower / Title Case buttons that transform the text
🔴 Advanced
- Add a Paste button that reads from the clipboard
- Save the text to
localStorageso it survives a page refresh - Add a keyword density chart — visual bars behind each keyword chip
Compare your solution: We built the full production version with target progress bar, case converters, copy/paste, and more — see the live Word Counter.
Full Source Code
Common Mistakes & Fixes
❌ "My word count is wrong on text with double spaces"
You're using split(" ") instead of split(/\s+/). The regex version handles any whitespace — double spaces, tabs, newlines.
❌ "The result flickers or feels laggy"
For very large text (100KB+), running the full update on every keystroke can feel slow. Fix it by debouncing — wait 100ms after the last keystroke before calculating:
var t; input.addEventListener("input", function() { clearTimeout(t); t = setTimeout(update, 100); });
❌ "Keywords include common words like 'the' and 'and'"
That's what the stopWords array is for. Make sure indexOf() check is running — the condition should skip those words entirely.
❌ "The character count is different from what Word says"
Different tools count differently. Some count spaces, some don't. Some treat double spaces as one, some as two. There's no single right answer — pick a convention and stick with it.