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.
What this skill does
# WXT
## 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
```bash
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
```typescript
// 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
```typescript
// 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)
```tsx
// 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
```bash
# 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 automatically
- **`storage` helper** — type-safe extension storage with `storage.getItem/setItem`
- **Hot reload works** — `npm run dev` reloads content scripts and popup on save
- **MV3 service workers** — background scripts are service workers (no persistent state)
- **`matches` for 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
Related 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.