What Is Plasmo?
Plasmo is a batteries-included framework for building browser extensions. Think of it as Next.js for extensions: hot reload, TypeScript out of the box, declarative content scripts, built-in messaging, and one-command publishing to the Chrome Web Store.
If you have ever wrestled with a raw manifest.json and webpack config just to get React working in a popup, Plasmo removes all that friction.
Prerequisites
- Node.js 18+ (LTS recommended)
- A Chromium-based browser (Chrome, Edge, Brave) for testing
- Basic React and TypeScript knowledge
1. Create Your First Extension
npm create plasmo@latest my-extension
cd my-extension
npm installPlasmo scaffolds a project with this structure:
my-extension/
├── src/
│ ├── popup.tsx # Extension popup UI
│ ├── background.ts # Service worker (background script)
│ ├── contents/ # Content scripts (injected into pages)
│ └── options.tsx # Options page
├── assets/ # Icons, images
├── package.json
└── tsconfig.json
No manifest.json — Plasmo generates it from your file structure and package.json metadata.
2. Build the Popup
The popup is the small UI that appears when a user clicks your extension icon. It is a standard React component.
import { useState } from "react"
function Popup() {
const [count, setCount] = useState(0)
return (
<div style={{
padding: 16,
minWidth: 300,
fontFamily: "system-ui, sans-serif"
}}>
<h1>My Extension</h1>
<p>You clicked the button {count} times.</p>
<button onClick={() =>
npm run devPlasmo launches a dev server with hot module replacement. Load the extension in Chrome:
- Go to
chrome://extensions - Enable "Developer mode"
- Click "Load unpacked"
- Select the
build/chrome-mv3-devfolder
Every save triggers a live reload inside the browser.
3. Content Scripts — Injecting UI into Web Pages
Content scripts let you modify or augment any webpage. Plasmo makes this declarative.
Create src/contents/overlay.tsx:
import type { PlasmoCSConfig } from "plasmo"
export const config: PlasmoCSConfig = {
matches: ["https://github.com/*"],
all_frames: true
}
function GitHubOverlay() {
return (
<div style={{
position: "fixed",
bottom: 16,
right: 16,
background: "#1a1a2e",
color: "#e0e0ff",
padding: "12px 20px",
borderRadius: 8
The config export tells Plasmo which URLs to inject this component into. No manual content_scripts array in a manifest.
You can use Tailwind CSS in content scripts too. Just install tailwindcss and Plasmo will handle the CSS injection automatically with shadow DOM isolation.
4. Background Service Worker
The background script runs persistently (Manifest V3 uses service workers). Use it for alarms, network interception, or global state.
// src/background.ts
export {}
chrome.runtime.onInstalled.addListener(() => {
console.log("Extension installed")
})
chrome.alarms.create("heartbeat", { periodInMinutes: 1 })
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "heartbeat") {
console.log("Heartbeat tick:", new Date().toISOString())
}
5. Messaging Between Popup and Content Script
Plasmo provides a type-safe messaging API that abstracts the raw chrome.runtime.sendMessage pattern.
// src/background/messages/getPageTitle.ts
import type { PlasmoMessaging } from "@plasmohq/messaging"
const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true
})
if (tab?.id) {
const [{ result }] = await chrome.scripting.executeScript({
target: {
Call it from the popup:
import { sendToBackground } from "@plasmohq/messaging"
async function fetchTitle() {
const resp = await sendToBackground({
name: "getPageTitle"
})
console.log("Page title:", resp.title)
}6. Storage API
Plasmo wraps chrome.storage with a reactive hook:
import { useStorage } from "@plasmohq/storage/hook"
function OptionsPage() {
const [theme, setTheme] = useStorage("theme", "dark")
return (
<div>
<h2>Settings</h2>
<select
value={theme}
onChange={(e) => setTheme(e.target.value)}
>
<option value="dark">Dark</option
Changes sync across popup, content scripts, and background — in real-time.
7. Adding Tailwind CSS
npm install tailwindcss @tailwindcss/typography postcss autoprefixer
npx tailwindcss init -pUpdate your tailwind.config.js content paths:
module.exports = {
content: ["./src/**/*.{tsx,ts,jsx,js}"],
theme: { extend: {} },
plugins: [],
}Import the CSS in your components and Plasmo handles the rest, including shadow DOM encapsulation for content scripts.
8. Building and Publishing
# Production build for Chrome
npm run build
# Build for Firefox
npm run build -- --target=firefox-mv2
# Package as .zip for Chrome Web Store
npm run packageThe resulting zip in build/ is ready to upload to the Chrome Web Store Developer Dashboard.
Plasmo also supports automated publishing via plasmo publish with a GitHub Action. Set your CWS_CLIENT_ID, CWS_CLIENT_SECRET, and CWS_REFRESH_TOKEN as repository secrets.
Project Ideas to Build Next
| Extension Idea | Key APIs |
|---|---|
| Tab manager | chrome.tabs, chrome.tabGroups, storage |
| AI summarizer | Content script + OpenAI API in background |
| Web3 wallet companion | Content script injection, window.ethereum |
| Screenshot tool | chrome.tabs.captureVisibleTab, Canvas API |
| Focus timer | chrome.alarms, badge text, notifications |
Wrapping Up
Plasmo eliminates the boilerplate that makes browser extension development painful. You get React, TypeScript, hot reload, and a sane project structure out of the box. Combined with the declarative content script system and typed messaging, it is the fastest path from idea to published extension.
Check out the Plasmo documentation and the Plasmo examples repository for more advanced patterns like CSUI (Content Script UI), side panels, and multi-browser builds.