plasmo
Expert guidance for Plasmo, the framework for building browser extensions with React, TypeScript, and modern tooling. Helps developers create Chrome and Firefox extensions with content scripts, background workers, popup UIs, and messaging — all with hot reload and zero webpack config.
What this skill does
# Plasmo — Browser Extension Framework
## Overview
Plasmo, the framework for building browser extensions with React, TypeScript, and modern tooling. Helps developers create Chrome and Firefox extensions with content scripts, background workers, popup UIs, and messaging — all with hot reload and zero webpack config.
## Instructions
### Project Setup
Create a new extension with Plasmo CLI:
```bash
# Create a new extension project
pnpm create plasmo my-extension
# Or with a specific template
pnpm create plasmo --with-tailwindcss my-extension
# Development with hot reload
pnpm dev
# Build for production
pnpm build
# Package for Chrome Web Store
pnpm package
```
### Popup UI
Build the extension popup as a React component:
```tsx
// src/popup.tsx — Main popup UI (click the extension icon)
import { useState, useEffect } from "react";
import { Storage } from "@plasmohq/storage";
const storage = new Storage();
function IndexPopup() {
const [savedCount, setSavedCount] = useState(0);
const [isEnabled, setIsEnabled] = useState(true);
useEffect(() => {
// Load state from extension storage (persists across sessions)
storage.get<number>("savedCount").then((count) => setSavedCount(count ?? 0));
storage.get<boolean>("isEnabled").then((enabled) => setIsEnabled(enabled ?? true));
}, []);
const toggleExtension = async () => {
const newState = !isEnabled;
setIsEnabled(newState);
await storage.set("isEnabled", newState);
// Notify content scripts about the state change
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (tabs[0]?.id) {
chrome.tabs.sendMessage(tabs[0].id, { type: "TOGGLE", enabled: newState });
}
};
return (
<div style={{ padding: 16, width: 320 }}>
<h2>My Extension</h2>
<p>Saved items: {savedCount}</p>
<button onClick={toggleExtension}>
{isEnabled ? "🟢 Enabled" : "🔴 Disabled"}
</button>
</div>
);
}
export default IndexPopup;
```
### Content Scripts
Inject UI and logic into web pages:
```tsx
// src/contents/overlay.tsx — Content script that renders a React component on any page
import type { PlasmoCSConfig, PlasmoGetOverlayAnchor } from "plasmo";
import { useState } from "react";
// Configure which pages this content script runs on
export const config: PlasmoCSConfig = {
matches: ["https://github.com/*"], // Only on GitHub
css: ["contents/overlay.css"], // Optional custom CSS
};
// Anchor the overlay to a specific DOM element (optional)
export const getOverlayAnchor: PlasmoGetOverlayAnchor = async () => {
return document.querySelector(".repository-content");
};
// The React component renders as an overlay on the page
function GitHubOverlay() {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="plasmo-overlay">
<button onClick={() => setIsOpen(!isOpen)}>
📋 Quick Actions
</button>
{isOpen && (
<div className="plasmo-panel">
<button onClick={() => copyRepoInfo()}>Copy repo info</button>
<button onClick={() => analyzeReadme()}>Analyze README</button>
</div>
)}
</div>
);
}
async function copyRepoInfo() {
const title = document.querySelector("[itemprop='name'] a")?.textContent?.trim();
const desc = document.querySelector("[itemprop='about']")?.textContent?.trim();
const stars = document.querySelector("#repo-stars-counter-star")?.textContent?.trim();
await navigator.clipboard.writeText(`${title}: ${desc} (⭐ ${stars})`);
}
export default GitHubOverlay;
```
### Background Service Worker
Handle long-running tasks, alarms, and cross-tab communication:
```typescript
// src/background.ts — Background service worker (Manifest V3)
import { Storage } from "@plasmohq/storage";
const storage = new Storage();
// Listen for messages from content scripts and popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
switch (message.type) {
case "SAVE_ITEM":
handleSaveItem(message.data).then(sendResponse);
return true; // Keep the message channel open for async response
case "GET_STATS":
getStats().then(sendResponse);
return true;
}
});
async function handleSaveItem(data: { url: string; title: string; content: string }) {
const items = (await storage.get<any[]>("savedItems")) ?? [];
items.push({ ...data, savedAt: Date.now() });
await storage.set("savedItems", items);
// Update badge count
const count = items.length;
chrome.action.setBadgeText({ text: count > 0 ? String(count) : "" });
chrome.action.setBadgeBackgroundColor({ color: "#6366f1" });
return { success: true, count };
}
// Set up periodic tasks with alarms
chrome.alarms.create("sync-data", { periodInMinutes: 30 });
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === "sync-data") {
const items = (await storage.get<any[]>("savedItems")) ?? [];
if (items.length > 0) {
// Sync to your backend API
await fetch("https://api.example.com/sync", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items }),
});
}
}
});
// Context menu (right-click menu)
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: "save-selection",
title: "Save selection to My Extension",
contexts: ["selection"],
});
});
chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === "save-selection" && info.selectionText) {
handleSaveItem({
url: tab?.url ?? "",
title: tab?.title ?? "",
content: info.selectionText,
});
}
});
```
### Messaging Between Components
Type-safe communication between popup, content scripts, and background:
```typescript
// src/messaging.ts — Type-safe messaging with Plasmo messaging API
import { sendToBackground, sendToContentScript } from "@plasmohq/messaging";
// Define message types
interface SaveRequest { url: string; title: string; }
interface SaveResponse { success: boolean; id: string; }
// From content script → background
async function saveFromContentScript(data: SaveRequest): Promise<SaveResponse> {
return sendToBackground({
name: "save-item", // Maps to src/background/messages/save-item.ts
body: data,
});
}
// From popup → content script
async function highlightElements() {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
return sendToContentScript({
name: "highlight",
tabId: tabs[0].id!,
body: { selector: "h1, h2, h3" },
});
}
```
```typescript
// src/background/messages/save-item.ts — Background message handler
import type { PlasmoMessaging } from "@plasmohq/messaging";
const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
const { url, title } = req.body;
// Process the save request
const id = crypto.randomUUID();
await chrome.storage.local.set({ [id]: { url, title, savedAt: Date.now() } });
res.send({ success: true, id });
};
export default handler;
```
### Storage with React Hooks
Reactive storage that syncs across extension components:
```tsx
// src/popup.tsx — Using storage hooks for reactive state
import { useStorage } from "@plasmohq/storage/hook";
function SettingsPopup() {
// useStorage provides React state that persists and syncs
// Changes in popup instantly reflect in content scripts and vice versa
const [theme, setTheme] = useStorage<string>("theme", "light");
const [apiKey, setApiKey] = useStorage<string>("apiKey", "");
const [savedItems, setSavedItems] = useStorage<any[]>("savedItems", []);
return (
<div style={{ padding: 16, width: 350 }}>
<h3>Settings</h3>
<label>Theme</label>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<label>API Key</labelRelated in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.