Word Count Tracker Chrome Extension (2026)

Chrome Extension Word Count Tracker: A Developer Guide

A word count tracker Chrome extension serves as a practical tool for writers, content creators, and developers who need to monitor text metrics across web pages. Whether you’re tracking article length, monitoring character counts in forms, or analyzing content density, building this extension teaches you fundamental Chrome extension development patterns that apply to countless other projects.

This guide walks you through creating a fully functional word count tracker from scratch, covering the manifest configuration, content script implementation, popup UI, and storage mechanisms. By the end, you will have a working extension you can load into Chrome and use immediately, plus a solid understanding of the architectural patterns that power more advanced extensions.

Understanding the Core Architecture

A word count tracker extension operates through three primary components working in concert. The content script analyzes text on the current page, the popup provides a quick-access interface for viewing statistics, and the background worker handles cross-tab communication and persistent storage.

The manifest file defines which permissions your extension requires and how each component interacts with web pages. For a word count tracker, you’ll need activeTab permission to access page content and storage to save user preferences.

Understanding message passing is critical before you write a single line of code. In Manifest V3, the popup and content script live in separate JavaScript contexts and cannot share variables directly. They communicate exclusively through chrome.runtime.sendMessage and chrome.tabs.sendMessage. This architecture is intentional. it enforces separation of concerns and keeps extensions secure.

Here is how data flows in a typical interaction:

  1. The user clicks the extension icon, which opens popup.html
  2. popup.js queries the active tab and sends a message to the content script
  3. The content script receives the message, runs the analysis, and calls sendResponse
  4. The popup receives the response and renders the stats to the DOM

This round-trip happens in milliseconds and gives you a clean model for building any extension that processes page content.

Comparing Extension Architectures

Before committing to a single approach, consider how different architectures affect performance and maintainability:

Approach When to Use Trade-offs
On-demand analysis (popup-triggered) Simple counters, occasional use Low overhead, no persistent memory
Continuous background monitoring Real-time goals, writing apps Higher memory use, works across tabs
MutationObserver with local state Dynamic SPAs, live editors Fast updates, requires DOM cleanup
Service worker with alarms Periodic stats, time-tracking Survives tab reloads, more complex setup

For a basic word count tracker, on-demand analysis is the right starting point. You can layer in MutationObserver support once the core works.

Setting Up Your Extension

Create a new directory for your extension project and add the manifest file:

{
 "manifest_version": 3,
 "name": "Word Count Tracker",
 "version": "1.0",
 "description": "Track word and character counts on any web page",
 "permissions": ["activeTab", "storage"],
 "action": {
 "default_popup": "popup.html",
 "default_icon": "icon.png"
 },
 "content_scripts": [{
 "matches": ["<all_urls>"],
 "js": ["content.js"]
 }]
}

This manifest grants your extension access to analyze text on any webpage while maintaining user privacy by requiring the active tab permission.

One important note about "<all_urls>" in the content_scripts matcher: this injects content.js into every page the user visits. That is fine for a word counter, but if your script becomes heavy, consider switching to programmatic injection using chrome.scripting.executeScript inside your popup so the script only runs when explicitly requested.

Your project directory should look like this after setup:

word-count-tracker/
 manifest.json
 content.js
 popup.html
 popup.js
 background.js (optional, for cross-tab features)
 icon.png

Implementing the Content Script

The content script runs within the context of web pages and performs the actual text analysis. Create a content.js file that extracts text from page elements:

function countWords(text) {
 const cleaned = text.trim();
 if (!cleaned) return 0;
 return cleaned.split(/\s+/).length;
}
function countCharacters(text, includeSpaces = false) {
 if (includeSpaces) {
 return text.length;
 }
 return text.replace(/\s/g, '').length;
}
function analyzePage() {
 const bodyText = document.body.innerText;
 const selection = window.getSelection().toString();
 return {
 page: {
 words: countWords(bodyText),
 characters: countCharacters(bodyText),
 charactersNoSpaces: countCharacters(bodyText, false)
 },
 selection: {
 words: countWords(selection),
 characters: countCharacters(selection),
 charactersNoSpaces: countCharacters(selection, false)
 }
 };
}
// Listen for messages from popup or background
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
 if (request.action === 'analyze') {
 const stats = analyzePage();
 sendResponse(stats);
 }
});

This content script provides two levels of analysis: full page statistics and selected text statistics. The message listener allows other extension components to request analysis on demand.

Improving Word Count Accuracy

The basic split(/\s+/) approach works for most cases but under-counts some languages and over-counts others. Here are three progressively more accurate strategies:

// Strategy 1: Basic split (good enough for Latin-script content)
function countWordsBasic(text) {
 return text.trim().split(/\s+/).filter(Boolean).length;
}
// Strategy 2: Strip punctuation before splitting (better for prose)
function countWordsCleaned(text) {
 const noPunct = text.replace(/[^\w\s'-]/g, ' ');
 return noPunct.trim().split(/\s+/).filter(w => w.length > 0).length;
}
// Strategy 3: Use Unicode word boundaries (best for multilingual content)
function countWordsUnicode(text) {
 // Matches sequences of Unicode word characters
 const matches = text.match(/\p{L}+/gu);
 return matches ? matches.length : 0;
}

For most content-writing use cases, Strategy 2 is the right balance of accuracy and simplicity. If you are building for international audiences or platforms that handle CJK characters, Strategy 3 is worth the extra complexity.

Counting Reading Time

A feature users frequently request is estimated reading time. The standard figure used by Medium and similar platforms is 200-250 words per minute for average adult readers:

function estimateReadingTime(wordCount, wpm = 225) {
 const minutes = wordCount / wpm;
 if (minutes < 1) return 'Less than 1 min read';
 const rounded = Math.ceil(minutes);
 return `${rounded} min read`;
}

Add this to your analyzePage return value and surface it in the popup.

Building the Popup Interface

The popup provides users with quick access to word count data without leaving their current page. Create popup.html:

<!DOCTYPE html>
<html>
<head>
 <style>
 body { width: 280px; padding: 16px; font-family: system-ui, sans-serif; }
 h2 { margin: 0 0 12px; font-size: 16px; }
 .stats { display: grid; gap: 8px; }
 .stat-row { display: flex; justify-content: space-between; }
 .label { color: #666; }
 .value { font-weight: 600; }
 .section { margin-top: 16px; padding-top: 12px; border-top: 1px solid #eee; }
 .section-title { font-size: 12px; color: #999; text-transform: uppercase; margin-bottom: 8px; }
 .reading-time { font-size: 11px; color: #888; margin-top: 4px; }
 .goal-bar { height: 6px; background: #eee; border-radius: 3px; margin-top: 8px; }
 .goal-fill { height: 100%; background: #4CAF50; border-radius: 3px; transition: width 0.3s; }
 </style>
</head>
<body>
 <h2>Word Count Tracker</h2>
 <div class="stats">
 <div class="stat-row">
 <span class="label">Words</span>
 <span class="value" id="pageWords">-</span>
 </div>
 <div class="stat-row">
 <span class="label">Characters</span>
 <span class="value" id="pageChars">-</span>
 </div>
 <div class="stat-row">
 <span class="label">Chars (no spaces)</span>
 <span class="value" id="pageCharsNoSpaces">-</span>
 </div>
 <div class="reading-time" id="readingTime"></div>
 <div class="goal-bar" id="goalBar" style="display:none">
 <div class="goal-fill" id="goalFill"></div>
 </div>
 </div>
 <div class="section">
 <div class="section-title">Selection</div>
 <div class="stat-row">
 <span class="label">Words</span>
 <span class="value" id="selWords">-</span>
 </div>
 <div class="stat-row">
 <span class="label">Characters</span>
 <span class="value" id="selChars">-</span>
 </div>
 </div>
 <script src="popup.js"></script>
</body>
</html>

Now create the popup JavaScript to communicate with the content script:

document.addEventListener('DOMContentLoaded', async () => {
 const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
 chrome.tabs.sendMessage(tab.id, { action: 'analyze' }, (response) => {
 if (chrome.runtime.lastError) {
 document.getElementById('pageWords').textContent = 'N/A';
 return;
 }
 if (response) {
 document.getElementById('pageWords').textContent =
 response.page.words.toLocaleString();
 document.getElementById('pageChars').textContent =
 response.page.characters.toLocaleString();
 document.getElementById('pageCharsNoSpaces').textContent =
 response.page.charactersNoSpaces.toLocaleString();
 document.getElementById('selWords').textContent =
 response.selection.words.toLocaleString();
 document.getElementById('selChars').textContent =
 response.selection.characters.toLocaleString();
 // Show reading time estimate
 const wpm = 225;
 const mins = Math.ceil(response.page.words / wpm);
 document.getElementById('readingTime').textContent =
 mins < 1 ? 'Less than 1 min read' : `~${mins} min read`;
 // Show goal progress if a goal is set
 chrome.storage.sync.get('wordCountGoal', ({ wordCountGoal }) => {
 if (wordCountGoal) {
 const pct = Math.min(100, (response.page.words / wordCountGoal) * 100);
 document.getElementById('goalBar').style.display = 'block';
 document.getElementById('goalFill').style.width = `${pct}%`;
 }
 });
 }
 });
});

Using toLocaleString() on the numbers automatically formats them with thousands separators (e.g., 12,450 instead of 12450), which is a small but appreciated usability improvement.

Adding Persistent Storage

To track word counts over time or save user preferences, use the Chrome storage API. Add configuration options for excluding certain elements:

// In popup.js - save preferences
async function savePreferences(settings) {
 await chrome.storage.sync.set({ wordCountSettings: settings });
}
// In content.js - apply settings
async function getSettings() {
 return new Promise((resolve) => {
 chrome.storage.sync.get('wordCountSettings', (result) => {
 resolve(result.wordCountSettings || {});
 });
 });
}
async function analyzePage() {
 const settings = await getSettings();
 const excludeSelectors = settings.excludeSelectors || ['script', 'style', 'nav', 'footer'];
 // Filter out unwanted elements
 const clone = document.body.cloneNode(true);
 excludeSelectors.forEach(selector => {
 clone.querySelectorAll(selector).forEach(el => el.remove());
 });
 const bodyText = clone.innerText;
 // ... rest of analysis
}

Choosing Between storage.sync and storage.local

Chrome gives you two storage areas for extensions, and picking the right one matters:

Feature storage.sync storage.local
Syncs across devices Yes (Chrome account required) No
Storage limit 100KB total, 8KB per item 10MB
Best for User preferences, goals Cached data, large fixtures
Write quota 1,800 writes/hour Unlimited
Use when Settings the user expects everywhere Page-specific data, session caches

For user preferences like excluded selectors and word count goals, storage.sync is the right choice. If you are storing per-URL word count history, use storage.local to avoid hitting the sync quota.

Tracking Word Count History

A useful feature is logging the word count each time the user opens the popup on a given URL, so they can see how content on a page changes over time:

async function logWordCount(url, wordCount) {
 const key = `history_${encodeURIComponent(url)}`;
 const existing = await chrome.storage.local.get(key);
 const history = existing[key] || [];
 history.push({ count: wordCount, timestamp: Date.now() });
 // Keep only the last 30 entries per URL
 const trimmed = history.slice(-30);
 await chrome.storage.local.set({ [key]: trimmed });
}

Call this from your popup after a successful sendMessage response.

Loading and Testing Your Extension

To test your extension in Chrome:

  1. Navigate to chrome://extensions/
  2. Enable “Developer mode” in the top right corner
  3. Click “Load unpacked” and select your extension directory
  4. Visit any webpage and click the extension icon to see word counts

The extension immediately analyzes the current page and displays statistics in the popup. You can also select text on the page to see counts for just that selection.

Common Errors and Fixes

When developing Chrome extensions, these are the most frequent problems you will encounter:

“Could not establish connection. Receiving end does not exist.” This error means your popup sent a message but the content script was not injected into the page. It happens on Chrome internal pages (chrome://, chrome-extension://) and the Chrome Web Store. Add a check:

const restrictedSchemes = ['chrome://', 'chrome-extension://', 'https://chrome.google.com'];
const isRestricted = restrictedSchemes.some(s => tab.url.startsWith(s));
if (isRestricted) {
 document.getElementById('pageWords').textContent = 'Not available';
 return;
}

Content script not updating after code changes. Chrome caches injected scripts. After editing content.js, go to chrome://extensions/, find your extension, and click the circular refresh icon. Then reload the target page.

Storage quota exceeded. If you are logging history for many URLs, the local storage can fill up. Add a cleanup routine that removes entries older than 30 days.

Extending the Functionality

Once the core word counting works, consider adding these enhancements:

  • Real-time updates: Use a MutationObserver to update counts when page content changes dynamically
  • Goals and targets: Allow users to set word count goals and visualize progress
  • Export functionality: Save statistics to a CSV file for tracking over time
  • Keyboard shortcut: Add a command shortcut for quick access
  • Dark mode: Match the popup styling to system preferences
// Real-time monitoring example
const observer = new MutationObserver((mutations) => {
 const stats = analyzePage();
 chrome.runtime.sendMessage({ action: 'updateStats', stats: stats });
});
observer.observe(document.body, {
 childList: true,
 subtree: true,
 characterData: true
});

Implementing a Word Count Goal with Visual Feedback

Goals are one of the most motivating features you can add. Here is a complete implementation that stores a goal in storage.sync and updates the progress bar in the popup:

// In popup.js. add a goal-setting form below the stats
function renderGoalForm(currentGoal) {
 const form = document.createElement('div');
 form.className = 'section';
 form.innerHTML = `
 <div class="section-title">Word Goal</div>
 <div style="display:flex;gap:8px;align-items:center">
 <input id="goalInput" type="number" min="0" step="100"
 value="${currentGoal || ''}" placeholder="e.g. 1500"
 style="width:80px;padding:4px 6px;border:1px solid #ccc;border-radius:4px">
 <button id="saveGoal"
 style="padding:4px 10px;background:#4CAF50;color:white;border:none;border-radius:4px;cursor:pointer">
 Save
 </button>
 </div>
 `;
 document.body.appendChild(form);
 document.getElementById('saveGoal').addEventListener('click', async () => {
 const goal = parseInt(document.getElementById('goalInput').value, 10);
 if (!isNaN(goal)) {
 await chrome.storage.sync.set({ wordCountGoal: goal });
 }
 });
}
// Load and render goal on popup open
chrome.storage.sync.get('wordCountGoal', ({ wordCountGoal }) => {
 renderGoalForm(wordCountGoal);
});

Adding a Keyboard Shortcut

Register a keyboard shortcut in manifest.json under the commands key:

"commands": {
 "_execute_action": {
 "suggested_key": {
 "default": "Ctrl+Shift+W",
 "mac": "Command+Shift+W"
 },
 "description": "Open word count popup"
 }
}

The _execute_action command is a special reserved name that triggers the extension action (opens the popup) without needing any additional JavaScript.

Publishing Considerations

If you plan to publish your extension on the Chrome Web Store, there are a few requirements to prepare for:

  • You need a 128x128 PNG icon, a 440x280 promotional image, and at least one screenshot
  • The Web Store review checks for overly broad permissions. justify "<all_urls>" in your listing description or switch to activeTab with programmatic injection
  • Increment the version field in manifest.json for every update you submit
  • Testing on Chrome Beta or Canary before submitting catches compatibility issues early

Building a word count tracker teaches you essential Chrome extension development skills that transfer directly to more complex projects. The patterns shown here. content script analysis, popup communication, storage integration, and real-time updates. form the foundation for building productivity tools, accessibility checkers, and data extraction extensions.


Try it: Estimate your monthly spend with our Cost Calculator.

I'm a solo developer in Vietnam. 50K Chrome extension users. $500K+ on Upwork. 5 Claude Max subscriptions running agent fleets in parallel. These are my actual CLAUDE.md templates, orchestration configs, and prompts. Not a course. Not theory. The files I copy into every project before I write a line of code. **[See what's inside →](https://zovo.one/lifetime?utm_source=ccg&utm_medium=cta-default&utm_campaign=chrome-extension-word-count-tracker)** $99 once. Free forever. 47/500 founding spots left.

Related Reading

Built by theluckystrike. More at zovo.one

Find the right skill → Browse 155+ skills in our Skill Finder.