Master Browser Extension Development with Plasmo: From Basics to Advanced Practices
This comprehensive guide explores the fundamentals and advanced techniques of building Chrome extensions using the Plasmo framework, covering plugin concepts, traditional and modern development workflows, code examples, dynamic UI injection, resource handling, and best practices for efficient, scalable extension engineering.
Sharing Goals
Help readers understand the breadth and depth of browser extensions, recognize their capabilities, and consider using them to improve work efficiency.
Share development practices and pitfalls when using frameworks like Plasmo, providing reusable knowledge for faster plugin development.
Origin
Our team needed heavy use of browser extensions for a recent project, prompting a thorough investigation of extension limits and how to integrate extension development into existing web engineering workflows. We discovered the Plasmo Framework, an engineering‑focused tool for building extensions, and this article introduces extensions, Plasmo‑based development, and practical business use cases.
Reading this article you will learn:
The why/what/how of browser extensions.
Traditional extension development processes.
The principles behind Plasmo.
How Plasmo changes the extension development workflow.
Business‑level development practices.
Step‑by‑step creation of your first extension.
About Browser Extensions
Chrome extensions are used as examples throughout.
Why Do We Need Browser Extensions?
Early browser vendors envisioned a "Browser OS" where extensions acted as desktop‑app‑like experiences, providing additional functionality, user retention mechanisms, and a competitive edge.
Extensions can:
Leverage browser traffic.
Offer application‑style experiences.
Encourage developers to build on the web stack.
Distribute via the Chrome Web Store.
Provide a direct entry point to users.
Foster a healthy ecosystem of third‑party tools.
Create a competitive barrier for the browser platform.
Chrome Web Store
Extension consumption entry point
Actual injection effect (script and UI injection)
What Is a Browser Extension?
In one sentence: a set of web‑based applications that customize the browser experience, run in a sandbox, and are distributed via the Chrome Web Store.
Architecture diagram of extensions, web pages, and their relationships.
Background Script
Content Script
Web Page
Popup Page
Option Page
Override Page
What Can Extensions Do?
In simple terms:
Anything a web page can do (render HTML/CSS, run JavaScript, manipulate the DOM).
Extensions also have exclusive APIs such as runtime, tabs, cookies, devtools, enabling tab management, custom devtools panels, context‑menu creation, bookmark handling, and more.
Tab Management
Right‑click Menu
Devtools
Search Bar
Reference the official Chrome extension samples for more use‑case ideas.
Reference: https://github.com/GoogleChrome/chrome-extensions-samples
About Plasmo Framework
Engineering‑Focused Development Flow
Traditional extension development uses raw HTML/CSS/JS and manual packaging. Modern developers expect a streamlined workflow with scaffolding, hot‑reloading, and integration with CI/CD.
Plasmo provides exactly that: a CLI that initializes a project, runs a dev server, builds deployable assets, and supports commands like init/dev/build.
Framework Principles
Plasmo is a Parcel‑based scaffolding that abstracts best practices for extension development while embracing modern frontend tooling.
Reference Parcel: https://parceljs.org/recipes/web-extension/
Plasmo repository: https://github.com/PlasmoHQ/plasmo
Project structure (simplified):
.
├── cli // CLI and scaffolding
│ ├── create-plasmo
│ │ └── src
│ └── plasmo
│ ├── i18n
│ ├── src
│ │ ├── commands
│ │ └── features
│ │ ├── extension-devtools
│ │ ├── extra
│ │ ├── helpers
│ │ └── manifest-factory
│ └── templates // React, Svelte, Vue templates
│ └── static
│ ├── react17
│ ├── react18
│ ├── svelte3
│ └── vue3
├── examples // sample extensions
│ ├── with-ant-design
│ └── with-background
├── extensions // real extensions
│ ├── mice
│ └── world-edit
├── packages // shared packages
│ ├── config
│ ├── constants
│ ├── init
│ ├── parcel-bundler
│ ├── parcel-config
│ └── ...
└── templates
└── qtt
└── src
└── __snapshots__Plasmo uses Turborepo for monorepo management and pnpm for package handling.
Turborepo is a high‑performance build system for JS/TS monorepos.
Plasmo‑based extension workflow diagram:
Engineering‑Focused Quick Start
Prerequisites : Node.js, pnpm, Git.
Node: https://nodejs.org/
Pnpm: https://pnpm.io/
Git: https://git-scm.com/
Project Initialization pnpm create plasmo The generated project contains a minimal React/TSX popup.
.
├── README.md
├── assets
│ └── icon512.png
├── node_modules
├── package.json
├── pnpm-lock.yaml
├── popup.tsx
└── tsconfig.jsonRunning the Project
cd hello-world
pnpm devOpen chrome://extensions, enable developer mode, and load the build folder to test the extension.
Make sure to enable developer mode and load the unpacked extension from build/chrome-mv3-dev .
After loading, the extension appears in the toolbar and can be interacted with.
Extension Development Quick Start (Traditional)
Key files:
manifest.json
background.js
popup.html
popup.jsSample manifest.json:
{
"name": "Drink Water Event Popup",
"description": "Demonstrates usage and features of the event page by reminding user to drink water",
"version": "1.0",
"manifest_version": 3,
"permissions": ["alarms","notifications","storage"],
"background": {"service_worker": "background.js"},
"action": {"default_title": "Drink Water Event","default_popup": "popup.html"},
"icons": {"16":"drink_water16.png","32":"drink_water32.png","48":"drink_water48.png","128":"drink_water128.png"}
}Sample background.js (uses alarms and notifications):
'use strict';
chrome.alarms.onAlarm.addListener(() => {
chrome.action.setBadgeText({text: ''});
chrome.notifications.create({
type: 'basic',
iconUrl: 'stay_hydrated.png',
title: 'Time to Hydrate',
message: 'Everyday I Guzzlin!'
});
});
chrome.notifications.onButtonClicked.addListener(async () => {
const item = await chrome.storage.sync.get(['minutes']);
chrome.action.setBadgeText({text: 'ON'});
chrome.alarms.create({delayInMinutes: item.minutes});
});Sample popup.html and popup.js provide UI for setting alarm intervals.
Plasmo‑Based Development
Plasmo abstracts away manual bundling. Create a .tsx file (e.g., popup.tsx) exporting a React component, and Plasmo generates the necessary HTML, CSS, and JS assets.
import { useState } from "react";
function Popup() {
const [data, setData] = useState("");
return (
<div style={{display: "flex", flexDirection: "column", padding: 16}}>
<h2>Welcome to your <a href="https://www.plasmo.com" target="_blank">Plasmo</a> Extension!</h2>
<input onChange={e => setData(e.target.value)} value={data} />
<a href="https://docs.plasmo.com" target="_blank">View Docs</a>
</div>
);
}
export default Popup;Plasmo automatically creates manifest.json entries, bundles assets, and supports hot‑reloading.
Extension UI Types
Extension Page UI (Popup, Options, Override) lives inside the extension context.
Injected UI (Content Script UI) is injected into web pages, often using Shadow DOM to avoid style leakage.
Static Injection with Plasmo
Create popup.tsx, options.tsx, or content.tsx and export the component. Plasmo adds the appropriate entries to manifest.json and bundles the code.
Runtime (Dynamic) Injection
When a page is already loaded, dynamically inject a content script using chrome.scripting.executeScript with world: "MAIN" to access the page's window object.
chrome.tabs.onActivated.addListener(activeInfo => {
const { tabId } = activeInfo;
chrome.tabs.query({active: true, currentWindow: true}, tabs => {
if (tabs[0]?.url?.includes('chrome://') || tabs[0]?.status !== 'complete') return;
const scriptPath = injectedContent.split('/').pop().split('?')[0];
chrome.scripting.executeScript({
target: { tabId: tabs[0].id },
files: [scriptPath]
});
});
});Resource Handling in Plasmo
Plasmo supports several import schemes: ~ resolves to the project root. url: loads a web‑accessible resource and adds it to manifest.json. data-base64: inlines assets as base64 strings. data-text: inlines raw text (e.g., CSS). chrome.runtime.getURL references files declared in web_accessible_resources without compilation.
Example of using url: to import a script: import myScript from "url:./path/to/file.js"; Example of inlining an image:
import logo from "data-base64:~assets/logo.png";
<img src={logo} alt="Logo" />Development Insights
Plasmo greatly reduces boilerplate and aligns extension development with modern web engineering practices. When encountering issues, consult Chrome extension API docs, Plasmo documentation, sample repositories, or the Plasmo Discord community.
References
Chrome extension samples: https://github.com/GoogleChrome/chrome-extensions-samples
Plasmo Framework: https://www.plasmo.com/
Turborepo: https://turborepo.org/
Plasmo docs: https://docs.plasmo.com/
Plasmo Discord: https://discord.com/invite/8rrxVYYtfd
Plasmo source code: https://github.com/PlasmoHQ/plasmo
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
