Simple Counter Extension¶
What it does¶
- Counts button clicks on any webpage
- Shows count in popup
- Saves count permanently
- Settings page to change highlight color and reset counter
- Shows badge number on icon
Full flow traced¶
- User clicks button on webpage
content.jsdetects it → highlights buttoncontent.jssends "incrementCounter" tobackground.jsbackground.jsgets count from storage → adds 1 → saves backbackground.jsupdates icon badge- User clicks extension icon → popup opens
popup.jsasks background for count- Shows in popup UI
- User opens settings → changes color → saves
background.jsdetects storage change → notifies all content scripts- Next button click uses new color
Extension Overview¶
What it does: - Counts how many times you click a button on web pages - Shows count in popup - Saves count so it persists - Has settings to reset counter or change button color - Highlights clicked buttons on page
File Structure¶
extension/
├── manifest.json
├── background.js
├── content.js
├── popup.html
├── popup.js
├── options.html
├── options.js
└── icon.png (16x16, 48x48, 128x128)
manifest.json¶
Purpose: Tells Chrome what this extension is and what it can do
{
"manifest_version": 3,
"name": "Click Counter",
"version": "1.0.0",
"description": "Counts button clicks on web pages",
"icons": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icon16.png",
"32": "icon32.png"
},
"default_title": "Click Counter"
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [{
"matches": ["https://*/*", "http://*/*"],
"js": ["content.js"],
"run_at": "document_idle"
}],
"permissions": [
"storage",
"activeTab"
],
"options_page": "options.html"
}
background.js¶
Purpose: Runs in background, handles events, coordinates everything. This is the "brain" - has full Chrome API access but no DOM access.
// This runs when extension is installed or updated
chrome.runtime.onInstalled.addListener((details) => {
console.log('Extension installed/updated');
if (details.reason === 'install') {
// First install - set default values
chrome.storage.local.set({
clickCount: 0,
highlightColor: '#ffff00'
});
console.log('Default values set');
}
});
// Listen for messages from content script or popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('Background received message:', message);
// Handle different message types
if (message.action === 'incrementCounter') {
// Content script says user clicked a button
// Get current count from storage
chrome.storage.local.get('clickCount', (result) => {
const newCount = (result.clickCount || 0) + 1;
// Save new count
chrome.storage.local.set({ clickCount: newCount }, () => {
console.log('Counter incremented to:', newCount);
// Send back the new count
sendResponse({ success: true, count: newCount });
// Update badge on extension icon to show count
chrome.action.setBadgeText({ text: String(newCount) });
chrome.action.setBadgeBackgroundColor({ color: '#4285f4' });
});
});
return true; // CRITICAL: Keeps message channel open for async sendResponse
}
if (message.action === 'getCount') {
// Popup wants to know current count
chrome.storage.local.get('clickCount', (result) => {
sendResponse({ count: result.clickCount || 0 });
});
return true;
}
if (message.action === 'resetCounter') {
// Options page wants to reset counter
chrome.storage.local.set({ clickCount: 0 }, () => {
console.log('Counter reset');
chrome.action.setBadgeText({ text: '' });
sendResponse({ success: true });
});
return true;
}
});
// Listen for storage changes (when options page updates settings)
chrome.storage.onChanged.addListener((changes, areaName) => {
console.log('Storage changed in area:', areaName);
if (changes.highlightColor) {
console.log('Highlight color changed from',
changes.highlightColor.oldValue,
'to',
changes.highlightColor.newValue
);
// Send message to all tabs to update their highlight color
chrome.tabs.query({}, (tabs) => {
tabs.forEach(tab => {
chrome.tabs.sendMessage(tab.id, {
action: 'updateColor',
color: changes.highlightColor.newValue
}).catch(() => {
// Tab might not have content script loaded, ignore error
});
});
});
}
});
content.js¶
Purpose: Runs inside web pages, can read/modify page DOM. This is the "hands" - can touch page but limited Chrome API access.
console.log('Content script loaded on:', window.location.href);
// Store current highlight color
let highlightColor = '#ffff00';
// Load highlight color from storage when script loads
chrome.storage.local.get('highlightColor', (result) => {
if (result.highlightColor) {
highlightColor = result.highlightColor;
console.log('Loaded highlight color:', highlightColor);
}
});
// Listen for clicks on the entire page
document.addEventListener('click', (event) => {
// Only care about button clicks
const target = event.target;
// Check if clicked element is a button or looks like one
if (target.tagName === 'BUTTON' ||
target.tagName === 'INPUT' && target.type === 'button' ||
target.tagName === 'INPUT' && target.type === 'submit') {
console.log('Button clicked:', target.textContent || target.value);
// Highlight the button
highlightButton(target);
// Tell background script to increment counter
chrome.runtime.sendMessage(
{ action: 'incrementCounter' },
(response) => {
if (response && response.success) {
console.log('Counter now at:', response.count);
// Show temporary notification on page
showNotification(`Button clicks: ${response.count}`);
}
}
);
}
});
// Function to highlight a button
function highlightButton(button) {
// Save original background color
const originalBg = button.style.backgroundColor;
// Apply highlight
button.style.backgroundColor = highlightColor;
button.style.transition = 'background-color 0.3s';
// Remove highlight after 1 second
setTimeout(() => {
button.style.backgroundColor = originalBg;
}, 1000);
}
// Function to show notification on page
function showNotification(message) {
// Create notification element
const notification = document.createElement('div');
notification.textContent = message;
// Style it
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4285f4;
color: white;
padding: 15px 20px;
border-radius: 5px;
font-family: Arial, sans-serif;
font-size: 14px;
z-index: 999999;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
animation: slideIn 0.3s;
`;
// Add to page
document.body.appendChild(notification);
// Remove after 2 seconds
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s';
setTimeout(() => notification.remove(), 300);
}, 2000);
}
// Listen for messages from background (e.g., color updates)
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('Content script received message:', message);
if (message.action === 'updateColor') {
// Options page changed the highlight color
highlightColor = message.color;
console.log('Highlight color updated to:', highlightColor);
}
if (message.action === 'getPageInfo') {
// Popup wants info about current page
sendResponse({
url: window.location.href,
title: document.title,
buttonCount: document.querySelectorAll('button').length
});
}
});
popup.html¶
Purpose: UI that shows when clicking extension icon
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
width: 300px;
padding: 20px;
font-family: Arial, sans-serif;
margin: 0;
}
h1 {
font-size: 18px;
margin: 0 0 15px 0;
color: #333;
}
.counter {
font-size: 48px;
font-weight: bold;
color: #4285f4;
text-align: center;
margin: 20px 0;
}
.info {
background: #f5f5f5;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
font-size: 12px;
color: #666;
}
button {
width: 100%;
padding: 10px;
background: #4285f4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
}
button:hover {
background: #3367d6;
}
.link {
text-align: center;
margin-top: 15px;
}
.link a {
color: #4285f4;
text-decoration: none;
font-size: 12px;
}
</style>
</head>
<body>
<h1>🖱️ Click Counter</h1>
<div class="counter" id="counter">0</div>
<div class="info" id="pageInfo">
Loading page info...
</div>
<button id="refreshBtn">🔄 Refresh Count</button>
<div class="link">
<a href="#" id="optionsLink">⚙️ Settings</a>
</div>
<script src="popup.js"></script>
</body>
</html>
popup.js¶
Purpose: Logic for popup UI. Popup reloads every time you open it - no persistent state here.
// Get DOM elements
const counterDisplay = document.getElementById('counter');
const pageInfo = document.getElementById('pageInfo');
const refreshBtn = document.getElementById('refreshBtn');
const optionsLink = document.getElementById('optionsLink');
// When popup loads, get current count
document.addEventListener('DOMContentLoaded', async () => {
console.log('Popup opened');
await loadCounter();
await loadPageInfo();
});
// Function to load counter value
async function loadCounter() {
chrome.runtime.sendMessage({ action: 'getCount' }, (response) => {
if (response) {
counterDisplay.textContent = response.count;
console.log('Loaded count:', response.count);
}
});
}
// Function to load page info
async function loadPageInfo() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab) {
pageInfo.textContent = 'No active tab';
return;
}
chrome.tabs.sendMessage(tab.id, { action: 'getPageInfo' }, (response) => {
if (chrome.runtime.lastError) {
pageInfo.textContent = 'Content script not available on this page';
return;
}
if (response) {
pageInfo.innerHTML = `
<strong>Page:</strong> ${response.title}<br>
<strong>Buttons:</strong> ${response.buttonCount}
`;
}
});
}
// Refresh button click handler
refreshBtn.addEventListener('click', async () => {
console.log('Refresh clicked');
await loadCounter();
await loadPageInfo();
});
// Options link click handler
optionsLink.addEventListener('click', (e) => {
e.preventDefault();
chrome.runtime.openOptionsPage();
window.close();
});
options.html¶
Purpose: Settings page for the extension
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Click Counter Settings</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 500px;
margin: 40px auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h1 {
margin-top: 0;
color: #333;
}
.setting {
margin: 20px 0;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #555;
}
input[type="color"] {
width: 100px;
height: 40px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.current-count {
background: #e3f2fd;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
font-size: 18px;
text-align: center;
color: #1976d2;
}
button {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-right: 10px;
}
.save-btn {
background: #4285f4;
color: white;
}
.save-btn:hover {
background: #3367d6;
}
.reset-btn {
background: #f44336;
color: white;
}
.reset-btn:hover {
background: #d32f2f;
}
.status {
margin-top: 15px;
padding: 10px;
border-radius: 4px;
display: none;
}
.status.show {
display: block;
}
.status.success {
background: #d4edda;
color: #155724;
}
</style>
</head>
<body>
<div class="container">
<h1>⚙️ Click Counter Settings</h1>
<div class="current-count">
Total clicks: <strong id="totalCount">0</strong>
</div>
<div class="setting">
<label for="colorPicker">Highlight Color:</label>
<input type="color" id="colorPicker" value="#ffff00">
<small style="color: #666; margin-left: 10px;">
Color used to highlight clicked buttons
</small>
</div>
<div>
<button class="save-btn" id="saveBtn">💾 Save Settings</button>
<button class="reset-btn" id="resetBtn">🔄 Reset Counter</button>
</div>
<div id="status" class="status"></div>
</div>
<script src="options.js"></script>
</body>
</html>
options.js¶
Purpose: Logic for options page
// Get DOM elements
const colorPicker = document.getElementById('colorPicker');
const totalCount = document.getElementById('totalCount');
const saveBtn = document.getElementById('saveBtn');
const resetBtn = document.getElementById('resetBtn');
const status = document.getElementById('status');
// Load current settings when page opens
document.addEventListener('DOMContentLoaded', loadSettings);
saveBtn.addEventListener('click', saveSettings);
resetBtn.addEventListener('click', resetCounter);
// Function to load settings from storage
function loadSettings() {
console.log('Loading settings...');
chrome.storage.local.get(['highlightColor', 'clickCount'], (result) => {
console.log('Loaded settings:', result);
if (result.highlightColor) {
colorPicker.value = result.highlightColor;
}
totalCount.textContent = result.clickCount || 0;
});
}
// Function to save settings
function saveSettings() {
console.log('Saving settings...');
const newColor = colorPicker.value;
chrome.storage.local.set({ highlightColor: newColor }, () => {
console.log('Settings saved:', newColor);
showStatus('Settings saved!');
});
}
// Function to reset counter
function resetCounter() {
if (!confirm('Reset click counter to 0?')) {
return;
}
console.log('Resetting counter...');
chrome.runtime.sendMessage({ action: 'resetCounter' }, (response) => {
if (response && response.success) {
console.log('Counter reset');
totalCount.textContent = '0';
showStatus('Counter reset to 0');
}
});
}
// Function to show status message
function showStatus(message) {
status.textContent = message;
status.className = 'status success show';
setTimeout(() => {
status.className = 'status';
}, 3000);
}
// Listen for storage changes
chrome.storage.onChanged.addListener((changes, areaName) => {
if (changes.clickCount) {
totalCount.textContent = changes.clickCount.newValue;
console.log('Counter updated to:', changes.clickCount.newValue);
}
});
Testing Instructions¶
1. Create Files¶
- Create a folder called "click-counter-extension"
- Create all files listed above
- Create 3 icon files (or use any small PNG images)
2. Load Extension¶
- Open Chrome
- Go to
chrome://extensions - Enable "Developer mode" (top right)
- Click "Load unpacked"
- Select your "click-counter-extension" folder
- Extension should appear with your icon
3. Test It¶
- Go to any website (e.g., google.com)
- Click any button on the page
- Button should flash yellow
- Notification should appear: "Button clicks: 1"
- Extension icon should show badge "1"
4. Check Popup¶
- Click extension icon in toolbar
- Popup should show counter and page info
- Click "Refresh Count" - counter updates
5. Check Options¶
- Click "Settings" in popup
- Options page opens
- Shows total count
- Change highlight color (e.g., to red #ff0000)
- Click "Save Settings"
- Go back to website, click button - should flash red now
- Click "Reset Counter" - counter goes to 0
6. Debugging¶
- Background script console:
chrome://extensions→ Click "service worker" - Content script console: Right-click page → Inspect → Console tab
- Popup console: Right-click popup → Inspect
7. What to Watch¶
- Click button on page → Check page console for "Button clicked"
- Check background console for "Counter incremented"
- Check storage:
chrome://extensions→ Click "Details" → "Inspect views: service worker" → Application tab → Storage → Local Storage - Should see
clickCountandhighlightColor
8. Tracing a Click¶
page button click
→ content.js detects (line with addEventListener)
→ sends message to background (chrome.runtime.sendMessage)
→ background.js receives (onMessage.addListener)
→ background increments counter in storage
→ background sends response back to content
→ content shows notification
→ background updates badge on icon
→ open popup → popup asks background for count
→ displays in popup UI