Chrome Extension Reading Progress — Developer Guide
8 min readBuild a Reading Progress Tracker Extension
This tutorial walks through building a Chrome extension that tracks reading progress, saves position, and displays reading statistics.
Step 1: Manifest Configuration
Create manifest.json with the required permissions:
{
"manifest_version": 3,
"name": "Reading Progress Tracker",
"version": "1.0",
"permissions": ["activeTab", "storage"],
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_idle"
}],
"action": {
"default_popup": "popup.html"
}
}
The activeTab permission allows access to the current tab, while storage enables persisting reading positions and stats.
Step 2: Content Script Setup
Create content.js to inject the progress bar:
// content.js
const progressBar = document.createElement('div');
progressBar.id = 'reading-progress-bar';
progressBar.style.cssText = `
position: fixed;
top: 0;
left: 0;
height: 4px;
background: #4CAF50;
width: 0%;
z-index: 999999;
transition: width 0.1s ease;
`;
document.body.appendChild(progressBar);
Step 3: Scroll Position Tracking
Calculate reading progress using scroll mathematics:
function getScrollProgress() {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight;
const winHeight = window.innerHeight;
const scrollPercent = scrollTop / (docHeight - winHeight);
return Math.min(Math.max(scrollPercent, 0), 1);
}
For article pages, target the specific content area instead of the entire document:
function getArticleContent() {
const selectors = ['article', '[role="main"]', '.post-content', '.article-body'];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el) return el;
}
return document.documentElement;
}
Step 4: Progress Bar Rendering
Update the progress bar width based on scroll position:
function updateProgressBar() {
const progress = getScrollProgress();
progressBar.style.width = `${progress * 100}%`;
// Update badge with percentage
chrome.runtime.sendMessage({
type: 'UPDATE_BADGE',
progress: Math.round(progress * 100)
});
}
Step 5: Save Position to Storage
Persist reading position keyed by URL:
function saveProgress() {
const progress = getScrollProgress();
const url = window.location.href;
chrome.storage.local.set({
[`progress_${url}`]: {
position: window.scrollY,
percentage: progress,
timestamp: Date.now(),
title: document.title
}
});
}
Step 6: Resume Reading Position
Restore saved position when revisiting a page:
function restoreProgress() {
const url = window.location.href;
chrome.storage.local.get(`progress_${url}`, (result) => {
if (result[`progress_${url}`]) {
const saved = result[`progress_${url}`];
// Small delay to ensure page is fully loaded
setTimeout(() => {
window.scrollTo(0, saved.position);
updateProgressBar();
}, 100);
}
});
}
// Call on page load
restoreProgress();
Step 7: Reading Statistics
Track reading time and statistics:
let startTime = Date.now();
let isReading = false;
function updateStats() {
const url = window.location.href;
const timeSpent = Math.floor((Date.now() - startTime) / 1000);
chrome.storage.local.get('readingStats', (result) => {
const stats = result.readingStats || {
totalTime: 0,
pagesRead: 0,
streak: 0,
lastReadDate: null
};
const today = new Date().toDateString();
if (stats.lastReadDate !== today) {
stats.streak = stats.lastReadDate ===
new Date(Date.now() - 86400000).toDateString()
? stats.streak + 1 : 1;
stats.lastReadDate = today;
}
stats.totalTime += timeSpent;
stats.pagesRead += 1;
chrome.storage.local.set({ readingStats: stats });
});
}
Step 8: Popup for Reading History
Create popup.html to display reading statistics:
<!DOCTYPE html>
<html>
<head>
<style>
body { width: 300px; padding: 16px; font-family: system-ui; }
.stat { margin: 8px 0; }
.stat-label { color: #666; }
.stat-value { font-weight: bold; font-size: 18px; }
h2 { margin-top: 0; }
</style>
</head>
<body>
<h2>📚 Reading Stats</h2>
<div class="stat">
<div class="stat-label">Total Reading Time</div>
<div class="stat-value" id="totalTime">0 min</div>
</div>
<div class="stat">
<div class="stat-label">Pages Read</div>
<div class="stat-value" id="pagesRead">0</div>
</div>
<div class="stat">
<div class="stat-label">Reading Streak</div>
<div class="stat-value" id="streak">0 days</div>
</div>
<script src="popup.js"></script>
</body>
</html>
// popup.js
chrome.storage.local.get('readingStats', (result) => {
const stats = result.readingStats || { totalTime: 0, pagesRead: 0, streak: 0 };
document.getElementById('totalTime').textContent = `${Math.floor(stats.totalTime / 60)} min`;
document.getElementById('pagesRead').textContent = stats.pagesRead;
document.getElementById('streak').textContent = `${stats.streak} days`;
});
Performance Optimization
Use passive listeners and throttling for smooth performance:
// Use passive listener for better scroll performance
window.addEventListener('scroll', () => {
updateProgressBar();
saveProgress();
}, { passive: true });
// Or use throttling (see patterns/throttle-debounce-extensions.md)
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
Badge Updates
Update the extension badge to show progress:
// background.js
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'UPDATE_BADGE') {
chrome.action.setBadgeText({ text: `${message.progress}%` });
chrome.action.setBadgeBackgroundColor({ color: '#4CAF50' });
}
});
Auto-Detection
Automatically detect article pages vs non-article pages:
function isArticlePage() {
const articleIndicators = [
'article', 'blog', 'post', 'story', 'news', 'documentation'
];
const url = window.location.href.toLowerCase();
const bodyClass = document.body.className.toLowerCase();
return articleIndicators.some(ind =>
url.includes(ind) || bodyClass.includes(ind)
) || document.querySelector('article, .article, .post-content');
}
Related Guides
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.