- Published on
We Should Build More Web Extensions
Chrome extensions (and browser extensions in general) are essentially small apps that live inside your browser. They don't try to replace websites or reinvent workflows, they just add minor conveniences that add up.
Why Extensions are Awesome
End users have little control over the decisions web developers make. Sometimes requested features go ignored—or conflict with business goals. That’s where extensions come in, they let users solve their own problems.
And because these extensions can be available across multiple websites you visit, they can be used to add minor conveniences that significantly compound. Think of ad-blockers or password managers.
And the best part, building them is way easier than you might think.
How Extensions Work — Simplified
At its core an extension is made of the following pieces:
A manifest.json file — which tells the browser what the extension does, what files it uses to do these things, what sites it can access, and what permissions it needs.
Some JavaScript — this is where the extension logic exists. It can interact with the DOM of web pages, make API calls, respond to user actions, and more.
HTML/CSS — for popups, options pages, or UI injected into web pages.
If you can build a web app, you can definitely build an extension. You can even use frameworks like Vue, Svelte or React to build out your extension, as long as the build process compiles down the output to a structure that the browser (through the manifest.json) understands.
Building a Simple Extension
I used to frequently check my google analytics dashboard to get an overview of how my apps have been performing relative to previous periods. I however found navigation between periods clunky, where I have to make 3 to 5 clicks to get to the next period.

So I built a simple extension that adds navigation buttons and allows me to quickly jump between periods. If I remember correctly, it barely took me 2 hours, but probably saved me more hours of clicking and frustration 😅.
Below is a quick overview of what this entailed:
# manifest.json
{
"manifest_version": 3,
"name": "GA Period Navigator",
"version": "1.0",
"description": "Adds navigation buttons to Google Analytics dashboard pages for quick period navigation instead of using the date picker.",
"permissions": ["activeTab"],
"content_scripts": [
{
"matches": ["https://analytics.google.com/*"],
"js": ["content.js"],
"css": ["styles.css"]
}
],
"icons": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
}
}
The manifest.json is what determines how the extension will be run. In this case, it entailed the following:
manifest_version: 3
This means the extension follows Manifest V3, which is a standard for Chrome extensions.
Each browser engine has their own version of the manifest format, so for example, Firefox uses a slightly different format.
name: "GA Period Navigator"
- This is the name that will appear in the browser's extension management page and Chrome Web Store.
version: "1.0"
- This helps with update management. Change this each time you make a new version of the extension.
description: "..."
- A brief explanation of what the extension does, which appears in the extension management page and store listings.
permissions: ["activeTab"]
- This specifies what browser features the extension needs access to.
activeTab
permission allows the extension to access the currently active tab, but only when the user invokes the extension. - Always ensure you access the minimum permissions required for your extension to function.
content_scripts
These are JavaScript files that run in the context of web pages:
- "matches": ["https://analytics.google.com/*"] - Matches specifies which websites the content scripts can run on (in this case, only Google Analytics pages).
- "js": ["content.js"] - This is the JS script file that will be injected into the matching pages.
- "css": ["styles.css"] - The CSS file that will be injected to style the extension's UI elements.
"icons"
- These are the icons that will be displayed in the extension management page and store listings and the browser
The javascript in the content.js
simply injects the buttons, and upon reading the current url, determines the date params to add and navigate to upon clicking "Next" or "Previous".
// Wait for the page to fully load
document.addEventListener('DOMContentLoaded', initialize);
window.addEventListener('load', initialize);
console.log('Loaded GA Period Navigator');
// Listen for URL changes since Google Analytics is a SPA
let lastUrl = location.href;
new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
initialize();
}
}).observe(document, { subtree: true, childList: true });
/**
* Initialize the extension after the page loads
*/
function initialize() {
// Wait a bit for Google Analytics to fully render
setTimeout(checkAndAddButtons, 1500);
}
/**
* Check for the date range picker and add navigation buttons
*/
function checkAndAddButtons() {
// Check if buttons already exist to avoid duplicates
if (document.getElementById('ga-nav-prev') || document.getElementById('ga-nav-next')) {
return;
}
// Look for the date range text element
const dateRangeText = document.querySelector('div[class*="primary-date-range-text"]');
if (!dateRangeText) {
// Try again later if the element isn't found
setTimeout(checkAndAddButtons, 1000);
return;
}
// Find the parent container to place our buttons
const dateContainer = dateRangeText.closest('div[data-testid="date-picker-container"]') ||
dateRangeText.parentElement.parentElement.parentElement;
if (!dateContainer) {
setTimeout(checkAndAddButtons, 1000);
return;
}
// Create container for our buttons
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'ga-period-nav-buttons';
// Create previous period button
const prevButton = document.createElement('button');
prevButton.id = 'ga-nav-prev';
prevButton.className = 'ga-nav-button';
prevButton.title = 'Previous Period';
prevButton.innerHTML = '⏮️';
prevButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
navigatePeriod(-1);
});
// Create next period button
const nextButton = document.createElement('button');
nextButton.id = 'ga-nav-next';
nextButton.className = 'ga-nav-button';
nextButton.title = 'Next Period';
nextButton.innerHTML = '⏭️';
nextButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
navigatePeriod(1);
});
// Add buttons to container
buttonsContainer.appendChild(prevButton);
buttonsContainer.appendChild(nextButton);
// Insert buttons next to date picker
dateContainer.parentNode.insertBefore(buttonsContainer, dateContainer.nextSibling);
}
The full code is available on github
Example of a React-based Extension
If the extension has more functionality, it makes sense to use a library or framework for better code organization and maintainability.
justTLDR is good example. It uses React and vite to compile the extension to the relevant files described in the manifest.json.
I built it to help me with quickly summarizing long articles and Youtube videos.
It links the content you're consuming to your preferred AI Chatbot and asks the bot to summarize. So it can convert a 2-hour long podcast to a 2 minute read.
In simple terms, it'll copy-paste the text from a web article or transcript from a Youtube video into your ChatGPT, Claude, Gemini, Grok or Deepseek account. Because it uses your own account, it's free. No API key, No subscriptions, No login required. Unlike the rest of the solutions out there.
If you're big on reading long articles, documentation, watching Youtube podcasts, it can come in handy.
Code is available on github
Final Thoughts
Chrome extensions are simple, but they can also be powerful, and deeply satisfying to build. For developers, they can give you an easy chance to craft tiny tools that directly improve your daily workflow.
If you haven’t built one yet, try it. Pick a small itch, scratch it and enjoy the quiet productivity boost that follows.