wxt
Build cross-browser extensions with WXT — the modern framework for Chrome, Firefox, Safari, and Edge extensions. Use when someone asks to "build a browser extension", "Chrome extension with React", "WXT framework", "cross- browser extension", "manifest v3 extension", "build Firefox extension", or "browser extension with TypeScript". Covers content scripts, background workers, popup/options pages, storage, messaging, and publishing.
Usage
Getting Started
- Install the skill using the command above
- Open your AI coding agent (Claude Code, Codex, Gemini CLI, or Cursor)
- Reference the skill in your prompt
- The AI will use the skill's capabilities automatically
Example Prompts
- "Review the open pull requests and summarize what needs attention"
- "Generate a changelog from the last 20 commits on the main branch"
Documentation
Overview
WXT is a Vite-based framework for building browser extensions — think "Next.js for extensions." File-based entrypoints, hot reload, TypeScript-first, and it outputs a single extension that works on Chrome (MV3), Firefox (MV2/MV3), Safari, and Edge. No more manually editing manifest.json or reloading the extension after every change.
When to Use
- Building a browser extension for Chrome, Firefox, or all browsers
- Want hot reload during development (not manual reload)
- Need TypeScript + React/Vue/Svelte in your extension
- Migrating from Manifest V2 to V3
- Want one codebase that targets multiple browsers
Instructions
Setup
npx wxt@latest init my-extension
cd my-extension
npm install
npm run dev # Opens Chrome with hot-reloading extension
Project Structure
my-extension/
├── entrypoints/
│ ├── popup/ # Popup UI (click extension icon)
│ │ ├── index.html
│ │ ├── main.tsx
│ │ └── App.tsx
│ ├── options/ # Options page
│ │ ├── index.html
│ │ └── main.tsx
│ ├── content.ts # Content script (runs on web pages)
│ └── background.ts # Service worker (background logic)
├── public/
│ └── icon/
│ ├── 16.png
│ ├── 48.png
│ └── 128.png
├── wxt.config.ts
└── package.json
Content Script
// entrypoints/content.ts — Runs on matched web pages
/**
* Content scripts have access to the DOM of the page.
* Define which URLs to match with the `matches` export.
*/
export default defineContentScript({
matches: ["*://*.github.com/*"], // Run on GitHub pages
main() {
// Add a custom button to every GitHub PR page
const prHeader = document.querySelector(".gh-header-actions");
if (prHeader) {
const btn = document.createElement("button");
btn.textContent = "🤖 AI Review";
btn.className = "btn btn-sm";
btn.onclick = async () => {
const diff = document.querySelector(".diff-view")?.textContent;
// Send to background for API call
const review = await browser.runtime.sendMessage({
type: "REVIEW_PR",
diff: diff?.slice(0, 5000),
});
alert(review.summary);
};
prHeader.prepend(btn);
}
},
});
Background Service Worker
// entrypoints/background.ts — Persistent background logic
/**
* Service worker handles API calls, alarms, and message routing.
* No DOM access here — communicate with content scripts via messaging.
*/
export default defineBackground(() => {
// Handle messages from content scripts
browser.runtime.onMessage.addListener(async (msg, sender) => {
if (msg.type === "REVIEW_PR") {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${await storage.getItem("local:apiKey")}`,
},
body: JSON.stringify({
model: "gpt-4o",
messages: [{ role: "user", content: `Review this diff:\n${msg.diff}` }],
}),
});
const data = await response.json();
return { summary: data.choices[0].message.content };
}
});
// Periodic tasks with alarms
browser.alarms.create("check-notifications", { periodInMinutes: 5 });
browser.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === "check-notifications") {
// Check for updates, show badge
const count = await checkNotifications();
if (count > 0) {
browser.action.setBadgeText({ text: String(count) });
}
}
});
});
Popup UI (React)
// entrypoints/popup/App.tsx — Extension popup with React
import { useState, useEffect } from "react";
export default function App() {
const [apiKey, setApiKey] = useState("");
const [saved, setSaved] = useState(false);
useEffect(() => {
storage.getItem<string>("local:apiKey").then((key) => {
if (key) setApiKey(key);
});
}, []);
const save = async () => {
await storage.setItem("local:apiKey", apiKey);
setSaved(true);
setTimeout(() => setSaved(false), 2000);
};
return (
<div style={{ width: 300, padding: 16 }}>
<h2>🤖 AI PR Reviewer</h2>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="OpenAI API Key"
style={{ width: "100%" }}
/>
<button onClick={save} style={{ marginTop: 8 }}>
{saved ? "✅ Saved!" : "Save Key"}
</button>
</div>
);
}
Build for Multiple Browsers
# Development (Chrome with hot reload)
npm run dev
# Development for Firefox
npm run dev:firefox
# Build for all browsers
npm run build # Chrome MV3
npm run build:firefox # Firefox MV2/MV3
# Zip for store submission
npm run zip
npm run zip:firefox
Examples
Example 1: Build a productivity extension
User prompt: "Build a Chrome extension that blocks distracting websites during focus time."
The agent will create a WXT extension with a popup for configuring blocked sites and focus timer, a content script that shows a block page on matched domains, and background alarms for timer management.
Example 2: Content enhancement extension
User prompt: "Build an extension that adds AI-powered summaries to any article page."
The agent will create a content script that detects article content, a floating sidebar UI, and background API calls to summarize text.
Guidelines
- File-based entrypoints — file name and location determine the extension component
browser.*API — WXT polyfills Chrome and Firefox differences automaticallystoragehelper — type-safe extension storage withstorage.getItem/setItem- Hot reload works —
npm run devreloads content scripts and popup on save - MV3 service workers — background scripts are service workers (no persistent state)
matchesfor content scripts — define URL patterns where scripts should inject- Message passing — content script ↔ background communication via
browser.runtime.sendMessage - One codebase, all browsers — build targets handle manifest differences
- Icons at 16/48/128px — required for Chrome Web Store
Information
- Version
- 1.0.0
- Author
- terminal-skills
- Category
- Development
- License
- Apache-2.0