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:
- The user clicks the extension icon, which opens
popup.html popup.jsqueries the active tab and sends a message to the content script- The content script receives the message, runs the analysis, and calls
sendResponse - 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:
- Navigate to
chrome://extensions/ - Enable “Developer mode” in the top right corner
- Click “Load unpacked” and select your extension directory
- 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 toactiveTabwith programmatic injection - Increment the
versionfield inmanifest.jsonfor 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.
Related Reading
- Chrome Extension Spending Tracker Chrome: A Developer’s Guide
- AI Citation Generator Chrome: A Developer Guide
- AI Color Picker Chrome Extension: A Developer’s Guide
Built by theluckystrike. More at zovo.one
Find the right skill → Browse 155+ skills in our Skill Finder.