Building Browser Extensions: Essentials

Not so long ago, I noticed that code examples on Medium don't have a copy code button. This makes users manually select and copy code, which is not convenient.
I checked Medium’s public repositories but couldn’t find a frontend codebase to contribute this feature directly. Browser extensions add functionality to websites you don't control. So, I decided to create a browser extension to make copying code from Medium articles more comfortable.
This is my first-ever browser extension. You can check out the code on GitHub or download from Chrome Web Store.
In this article, I’ll cover what you need to know to build browser extensions. I’ll be using the Chrome extension as an example to illustrate how to add a copy button to code blocks on web pages. But the same solution will work fine for Firefox too (some changes to manifest file will be required). This article is for developers who know how to code but have never built a browser extension before.
How Extensions Are Different from Regular Web Apps
Web apps are served from an origin and executed by the browser under the browser’s security model. Browser extensions are guests on someone else's page.
Extensions operate under a manifest-based permission model and can have capabilities that regular web apps do not have. At the same time, the capabilities are explicit and controlled by the browser.
This introduces challenges because you don’t control the environment: browser controlls the lifecycle, the page’s JavaScript and CSS might conflict with your extension, and changes to the DOM structure can break its appearance and behavior at any time.
Modern Extension Architecture Overview
Manifest File
When a browser loads an extension, it starts with reading a configuration file: manifest.json. This file provides the browser with details about the extension: its name, permissions it needs, scripts to run, and where to run them.
The browser uses this file to determine the extension’s capabilities, the websites it should run on, and the resources it includes.
This configuration file is a part of the WebExtensions API, a cross-browser standard. V3 is the current standard, older versions are phased out. Firefox, Edge, and other Chromium-based browsers support this manifest format. This means, extensions developed for Chrome can often run in other browsers with minimal or no changes.
Core Components
Browser extensions consist of several components, each of which serves a distinct purpose.
Popup and options pages are HTML-based UI elements for extensions. A popup appears when a user clicks the extension’s icon in the browser toolbar. Options pages allow users to configure the extension’s settings and preferences. Both are built with standard HTML, CSS, and JavaScript, just like regular web pages. Since my copy button extension doesn’t require user configuration or interaction beyond clicking the button itself, I won’t implement these UI components.
Content scripts are JavaScript files that inject directly into web pages as they load. When a user visits a website that matches the extension's permissions, the browser automatically runs the content script in the context of that page. This means the extension has full access to the page's DOM - it can read, modify, or add elements, as well as listen for events. However, the extension cannot access the page's JavaScript variables or functions and websites cannot interfere with the extension's logic. I will be using it to add the copy button.
Background service workers handle event-driven logic of extensions. They can't access or modify any page's DOM directly, but they can manage state, respond to browser events, and coordinate communication between different parts of an extension.
Setting Up a Modern Build System
You can build a Chrome extension with just vanilla JavaScript: create .js files, reference them in your manifest, and you're done. But as your extension grows, you'll quickly miss the conveniences that modern build tools provide, because traditional extension development requires you to edit a file, go to chrome://extensions, click the reload button, then refresh the webpage to see your changes. I missed Hot Module Replacement (HMR), so I decided to set up Vite.
Vite gives you HMR, which means when you change a file during development, you see the results instantly without manually reloading anything. With Vite, you just save the file. Beyond improving the development experience, Vite handles modern JavaScript complexities: ES modules, TypeScript, minification and tree-shaking.
However, out of the box, Vite doesn't support HMR in the context of browser extensions development, requiring extra configuration: extensions can have multiple entry points.
To simplify setup, I used CRXJS plugin. It configures Vite to handle multiple entry points for extensions, integrates with Vite's HMR to provide live updates, updates the manifest file during build process.
Building A Copy Button Extension
As I already mentioned, in order to add a button to someone's page content you need to use the content scripts. Let's start.
Project Structure
The project sturcture includes configuration files in the root, public folder with static assets and src folder. In the src folder each major component of your extension gets its own folder, making it immediately clear what code lives there.
The src/content/ folder is named after the type of script it contains - content scripts. This naming convention keeps the code organized as the extension grows. If you later add a background service worker, you'd create src/background/ for those files.
my-extension/
├── public/ # Static assets
│ ├── icon16.png
│ ├── icon48.png
│ └── icon128.png
├── src/
│ └── content/ # Content script code
│ ├── content.js # Main content script
│ ├── content.css
│ ├── copy-button.html
│ ├── utils.js
│ └── config.js
├── dist/ # Build output (auto-generated)
├── manifest.json
├── package.json
└── vite.config.js
The public/ folder holds static assets, the dist/ folder is generated automatically by Vite and contains your built extension ready to load in Chrome.
Step-by-Step Setup
- Initialize the project & install dev dependencies
bash1mkdir chrome-copy-button2cd chrome-copy-button34npm init5npm install -D vite @crxjs/vite-plugin
- Update package.json
json1{2 "name": "chrome-copy-button",3 "version": "1.0.0",4 "description": "Chrome extension to add copy buttons to code blocks",5 "scripts": {6 "dev": "vite",7 "build": "vite build",8 "preview": "vite preview"9 },10 "keywords": ["chrome-extension", "copy", "code"],11 "author": "Your Name",12 "license": "MIT",13 "devDependencies": {14 "@crxjs/vite-plugin": "^2.3.0",15 "vite": "^7.3.1"16 }17}
- Create
manifest.json
json1{2 "manifest_version": 3,3 "name": "Code Copy Button",4 "version": "1.0.0",5 "description": "Adds a copy button to code blocks on web pages",6 "permissions": [],7 "host_permissions": [],8 "content_scripts": [9 {10 "matches": ["https://medium.com/*", "https://*.medium.com/*"],11 "js": ["src/content/content.js"],12 "run_at": "document_idle"13 }14 ],15 "icons": {16 "16": "icon16.png",17 "48": "icon48.png",18 "128": "icon128.png"19 }20}
The version, name, and description fields are straightforward metadata.
The permissions array is where you declare which browser APIs your extension needs access to. Our extension doesn't need any special permissions to write to the clipboard because we're using the modern Clipboard API, which works without explicit permission grants.
The host_permissions array specifies which websites your extension can interact with.
The content_scripts array defines which JavaScript files to inject and where to inject them. The matches field specifies target websites. The js array lists the files to inject.
The run_at field specifies when during the page load lifecycle the browser should inject and execute your content script. document_idle means browser will run extension after the DOM is fully constructed and the page is interactive. Alternative timing options include document_start for intercepting very early in the page lifecycle, and document_end for running right after DOM construction but potentially before the page is interactive, though for most use cases document_idle strikes the best balance.
The icons object provides three image sizes that Chrome uses in different contexts. The 16px icon appears in the browser toolbar, the 48px version shows on the extensions management page, and the 128px version is used in the Chrome Web Store and during installation prompts.
This article is not about creating icons, so I skip this part, you can create them in Figma or download free icons from corresponding web resources.
- Create
vite.config.js
It allows to configure and to customize Vite’s behavior to suit the project’s needs. We need to enable plugins and pass the manifest file to CRXJS plugin.
javascript1import { defineConfig } from 'vite';2import { crx } from '@crxjs/vite-plugin';3import manifest from './manifest.json';45export default defineConfig({6 plugins: [crx({ manifest })],7 build: {8 outDir: 'dist',9 },10});
- Create initial content script
src/content/content.js:
javascript1'use strict';23console.log('Copy button extension loaded!');45function init() {6 console.log('Initializing copy button extension...');7}89init();
Since we're using Vite as our build tool, the content script can be written directly as an ES module without wrapping it in an Immediately Invoked Function Expression (IIFE). Vite generates the IIFE during the build process.
- Running in Development Mode
bash1npm run dev
After running the development server, you'll see output indicating that Vite has started successfully and built your extension into the dist/ folder.
Loading in Chrome
- Open Chrome and go to
chrome://extensions/ - Enable "Developer mode" (toggle in top-right)
- Click "Load unpacked"
- Select your
dist/folder
The extension is now installed. To verify it is working, go to any Medium article and open the browser's DevTools Console and check for “Copy button extension loaded!” message. You should see the message, confirming that your content script successfully injected into the page.
This is a part of my series on building browser extensions.
Next article: Building Isolated UI Components for Browser Extensions