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
▶ Live Preview — this is what you'll build

What You'll Learn

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.

1

Build the HTML Structure

Create index.html. The layout is a textarea for input, a grid of stat cards, and a keywords list underneath.

HTML
<!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.

2

Style It with CSS

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; }
3

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.

JavaScript
// 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.

4

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:

JavaScript
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:

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.

5

Practice Challenges

🟢 Beginner

🟡 Intermediate

🔴 Advanced

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.

What to Build Next