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.

ELab Team
ELab Team
ELab Team
Master Browser Extension Development with Plasmo: From Basics to Advanced Practices

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.json

Running the Project

cd hello-world
pnpm dev

Open 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.js

Sample 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

TypeScriptReActWeb developmentbrowser extensionfrontend engineeringPlasmoChrome API
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.