Loading...
Complete guide to building Chrome extensions with Manifest V3 — architecture, APIs, popup UI, content scripts, background workers, storage, and publishing to Chrome Web Store.
Chrome Extension Structure:
┌──────────────────────────────────────┐
│ Service Worker │
│ (background.js) │
│ Runs in background, event-driven │
└──────────┬───────────────┬───────────┘
│ │
┌──────▼──────┐ ┌─────▼──────────┐
│ Popup UI │ │ Content Script │
│ (popup.html) │ │ (content.js) │
│ User clicks │ │ Injected into │
│ extension │ │ web pages │
└─────────────┘ └────────────────┘
my-extension/
├── manifest.json # Extension config
├── background.js # Service worker
├── popup/
│ ├── popup.html # Popup UI
│ ├── popup.css # Popup styles
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0.0",
"description": "A useful Chrome extension",
"permissions": [
"activeTab",
"tabs",
"storage",
"alarms",
"idle"
],
"host_permissions": [
"https://your-api.com/*"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content/content.js"],
"run_at": "document_idle"
}
],
"options_page": "options/options.html",
"icons": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
}
// background.js
// Listen for tab changes
chrome.tabs.onActivated.addListener(async (activeInfo) => {
const tab = await chrome.tabs.get(activeInfo.tabId);
console.log("Active tab:", tab.url);
// Track active time
await trackTime(tab.url, tab.title);
});
// Periodic heartbeat
chrome.alarms.create("heartbeat", { periodInMinutes: 1 });
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === "heartbeat") {
await sendHeartbeat();
}
});
// Idle detection
chrome.idle.onStateChanged.addListener((newState) => {
if (newState === "idle" || newState === "locked") {
pauseTracking();
} else {
resumeTracking();
}
});
// Track time helper
async function trackTime(url, title) {
const data = await chrome.storage.local.get("sessions");
const sessions = data.sessions || [];
const domain = new URL(url).hostname;
const existing = sessions.find(s => s.domain === domain);
if (existing) {
existing.totalSeconds += 60;
existing.lastSeen = Date.now();
} else {
sessions.push({
domain,
title,
totalSeconds: 60,
firstSeen: Date.now(),
lastSeen: Date.now(),
});
}
await chrome.storage.local.set({ sessions });
}
// Send data to server
async function sendHeartbeat() {
const { apiUrl, apiToken, sessions } = await chrome.storage.local.get([
"apiUrl",
"apiToken",
"sessions",
]);
if (!apiUrl || !apiToken || !sessions?.length) return;
try {
await fetch(`${apiUrl}/api/time-tracking/heartbeat`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiToken}`,
},
body: JSON.stringify({ sessions }),
});
// Clear synced data
await chrome.storage.local.set({ sessions: [] });
} catch (error) {
console.error("Heartbeat failed:", error);
}
}
<!-- popup/popup.html -->
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="popup-container">
<h1>Hours Tracker</h1>
<div class="project-selector">
<label>Active Project</label>
<select id="project-select">
<option value="">Select project...</option>
</select>
</div>
<div class="today-summary">
<h2>Today</h2>
<div id="total-time" class="big-number">0h 0m</div>
<div id="domain-list" class="domain-list"></div>
</div>
<div class="actions">
<button id="pause-btn">Pause</button>
<button id="settings-btn">Settings</button>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
// popup/popup.js
document.addEventListener("DOMContentLoaded", async () => {
const data = await chrome.storage.local.get("sessions");
const sessions = data.sessions || [];
// Calculate today's total
const totalSeconds = sessions.reduce((sum, s) => sum + s.totalSeconds, 0);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
document.getElementById("total-time").textContent = `${hours}h ${minutes}m`;
// Show domain breakdown
const list = document.getElementById("domain-list");
const sorted = sessions.sort((a, b) => b.totalSeconds - a.totalSeconds);
sorted.slice(0, 5).forEach(session => {
const mins = Math.floor(session.totalSeconds / 60);
const div = document.createElement("div");
div.className = "domain-item";
div.innerHTML = `
<span class="domain">${session.domain}</span>
<span class="time">${mins}m</span>
`;
list.appendChild(div);
});
});
// content/content.js
// Inject into web pages for advanced tracking
let lastActivity = Date.now();
// Track user activity
document.addEventListener("click", () => { lastActivity = Date.now(); });
document.addEventListener("keydown", () => { lastActivity = Date.now(); });
document.addEventListener("scroll", () => { lastActivity = Date.now(); });
// Report activity to background
setInterval(() => {
const idleSeconds = (Date.now() - lastActivity) / 1000;
chrome.runtime.sendMessage({
type: "activity",
idle: idleSeconds > 300, // 5 min idle
domain: window.location.hostname,
title: document.title,
});
}, 30000); // Every 30 seconds
// Persistent storage across extension components
// Save settings
await chrome.storage.sync.set({
apiUrl: "https://hardikkanajariya.in",
idleTimeout: 5,
trackIncognito: false,
});
// Read settings
const settings = await chrome.storage.sync.get([
"apiUrl",
"idleTimeout",
"trackIncognito",
]);
// Local storage (larger, not synced)
await chrome.storage.local.set({ sessions: [] });
// Listen for changes
chrome.storage.onChanged.addListener((changes, area) => {
if (area === "sync" && changes.apiUrl) {
console.log("API URL changed from", changes.apiUrl.oldValue, "to", changes.apiUrl.newValue);
}
});
Chrome Web Store requirements:
1. Developer account ($5 one-time fee)
2. Extension packaged as .zip
3. Store listing assets:
- 128x128 icon
- 1280x800 screenshot(s)
- Detailed description
- Privacy policy URL
4. Review process (1-7 days)
document or windowsync is 100KB, local is 10MBchrome://extensions → Load unpackedOur Hours Tracker Chrome Extension ($19) uses all these patterns professionally. Read the product deep dive.
Related reads:
Follow on X/Twitter for Chrome extension development tips.
From tutorial to product. Our Hours Tracker Chrome Extension started exactly like this — now it's a $19 product. Build yours today.
Get the latest articles, tutorials, and updates delivered straight to your inbox. No spam, unsubscribe at any time.