Why Build a Chrome Extension in 2026?
Chrome extensions remain one of the most underrated side projects for developers. With over 3 billion Chrome users worldwide, even a niche extension can reach thousands of users organically. And with AI coding tools in 2026, the barrier to building one has dropped to near zero.
Here's why now is the perfect time:
- Manifest V3 is stable — the migration chaos is over, the API is mature
- AI handles the boilerplate — focus on your unique idea, not manifest.json syntax
- Monetization paths are clear — freemium, subscriptions, and affiliate models all work
- Portfolio differentiator — published extensions impress hiring managers more than another todo app
What We'll Build
We'll build a "Reading Time Estimator" extension that:
- Detects article content on any webpage
- Calculates estimated reading time
- Shows a floating badge with the time estimate
- Saves reading history to local storage
- Has a popup showing your reading stats
This is simple enough to build in 30 minutes but complex enough to demonstrate all the key extension concepts.
Step 1: Scaffold with AI (5 minutes)
Open your AI coding agent and give it this prompt:
Create a Chrome extension (Manifest V3) called "ReadTime" that:
1. Content script: detects main article text, calculates reading time at 238 WPM
2. Shows a floating badge in the bottom-right corner with "X min read"
3. Service worker: stores reading history per domain in chrome.storage.local
4. Popup: shows total articles read, average reading time, top 5 domains
5. Use TypeScript, compile to JS for the extension
6. Include proper manifest.json with minimal permissionsWithin minutes, the AI will generate the full project structure:
readtime-extension/
├── manifest.json
├── src/
│ ├── content.ts # Injected into web pages
│ ├── background.ts # Service worker
│ ├── popup.ts # Popup logic
│ └── popup.html # Popup UI
├── styles/
│ └── badge.css # Floating badge styles
├── icons/
│ ├── icon16.png
│ ├── icon48.png
│ └── icon128.png
└── tsconfig.jsonStep 2: Understanding manifest.json (3 minutes)
The manifest is the heart of every extension. Here's what a minimal V3 manifest looks like:
{ "manifest_version": 3, "name": "ReadTime - Reading Time Estimator", "version": "1.0.0", "description": "See how long any article will take to read", "permissions": ["storage", "activeTab"], "action": { "default_popup": "popup.html", "default_icon": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" } }, "content_scripts": [{ "matches": ["<all_urls>"], "js": ["dist/content.js"], "css": ["styles/badge.css"] }], "background": { "service_worker": "dist/background.js" }
}Key Manifest V3 differences from V2:
service_workerreplacesbackground.scripts— no persistent background pageactionreplaces bothbrowser_actionandpage_actionhost_permissionsis separate frompermissionsfor URL access- No remote code — you can't load scripts from external URLs
Step 3: The Content Script (7 minutes)
The content script runs on every page and calculates reading time:
// content.ts
const WORDS_PER_MINUTE = 238; function getArticleText(): string { // Try common article selectors const selectors = ['article', '[role="main"]', '.post-content', '.article-body', 'main']; for (const selector of selectors) { const el = document.querySelector(selector); if (el && el.textContent && el.textContent.trim().length > 500) { return el.textContent.trim(); } } // Fallback: use body text minus nav/footer const body = document.body.cloneNode(true) as HTMLElement; body.querySelectorAll('nav, footer, header, aside, script, style').forEach(el => el.remove()); return body.textContent?.trim() || '';
} function calculateReadTime(text: string): number { const words = text.split(/\s+/).filter(w => w.length > 0).length; return Math.max(1, Math.ceil(words / WORDS_PER_MINUTE));
} function showBadge(minutes: number): void { const badge = document.createElement('div'); badge.id = 'readtime-badge'; badge.textContent = `${minutes} min read`; document.body.appendChild(badge);
} // Main execution
const text = getArticleText();
if (text.length > 500) { const minutes = calculateReadTime(text); showBadge(minutes); // Report to service worker chrome.runtime.sendMessage({ type: 'ARTICLE_READ', domain: window.location.hostname, minutes, url: window.location.href, title: document.title });
}Step 4: The Service Worker (5 minutes)
In Manifest V3, service workers replace background pages. The critical difference: service workers are ephemeral. They can be terminated at any time and must persist state to chrome.storage.
// background.ts
interface ReadingEntry { domain: string; minutes: number; url: string; title: string; timestamp: number;
} chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'ARTICLE_READ') { const entry: ReadingEntry = { domain: message.domain, minutes: message.minutes, url: message.url, title: message.title, timestamp: Date.now() }; chrome.storage.local.get(['readingHistory'], (result) => { const history: ReadingEntry[] = result.readingHistory || []; history.push(entry); // Keep last 1000 entries const trimmed = history.slice(-1000); chrome.storage.local.set({ readingHistory: trimmed }); }); }
});Step 5: The Popup UI (5 minutes)
The popup shows reading statistics when the user clicks the extension icon:
// popup.ts
document.addEventListener('DOMContentLoaded', () => { chrome.storage.local.get(['readingHistory'], (result) => { const history = result.readingHistory || []; // Total articles document.getElementById('total')!.textContent = String(history.length); // Average reading time const avgMinutes = history.length > 0? Math.round(history.reduce((sum, e) => sum + e.minutes, 0) / history.length): 0; document.getElementById('average')!.textContent = `${avgMinutes} min`; // Top domains const domainCounts: Record<string, number> = {}; history.forEach(e => { domainCounts[e.domain] = (domainCounts[e.domain] || 0) + 1; }); const topDomains = Object.entries(domainCounts).sort((a, b) => b[1] - a[1]).slice(0, 5); const list = document.getElementById('domains')!; topDomains.forEach(([domain, count]) => { const li = document.createElement('li'); li.textContent = `${domain} (${count} articles)`; list.appendChild(li); }); });
});Step 6: Test & Publish (5 minutes)
Local Testing
- Compile TypeScript:
npx tsc - Open
chrome://extensionsin Chrome - Enable "Developer mode" (top right toggle)
- Click "Load unpacked" and select your extension folder
- Navigate to any article — you should see the reading time badge
Chrome Web Store Submission
- Create a developer account at the Chrome Web Store Developer Dashboard (one-time $5 fee)
- Zip your extension folder (excluding
node_modulesand source.tsfiles) - Upload the zip, add screenshots, and write a description
- Submit for review — typically approved within 1-3 business days
Tips & Common Pitfalls
- Request minimal permissions — extensions with fewer permissions get approved faster and users trust them more
- Handle service worker lifecycle — don't store state in variables; always use
chrome.storage - Test on multiple sites — content scripts can break on sites with strict CSP (Content Security Policy)
- Add error boundaries — wrap content script logic in try/catch to prevent breaking host pages
- Use
activeTabpermission instead of<all_urls>when possible — it's less scary for users
Monetization Strategies
Once your extension has users, here are ethical monetization approaches:
- Freemium — free basic features, paid pro features (most common)
- One-time purchase — charge $1-5 via Chrome Web Store Payments
- Subscription — monthly fee for premium features via your own Stripe integration
- Affiliate links — recommend relevant tools in the extension's options page
Avoid: injecting ads into web pages, collecting user data without consent, or modifying search results. These violate Chrome Web Store policies and will get your extension banned.