Scaffold and setup Chrome MV3 extensions using WXT framework with React, TypeScript, and shadcn-UI. Use when creating new browser extensions, setting up content scripts, background service workers, side panels, popups, or configuring native messaging. Supports Google Docs/Overleaf integrations, DOM extraction, and cross-context communication patterns.
Scaffolds Chrome MV3 extensions with WXT, React, and shadcn-UI for browser automation.
/plugin marketplace add kjgarza/marketplace-claude/plugin install kjgarza-senior-software-developer-plugins-senior-software-developer@kjgarza/marketplace-claudeThis skill inherits all available tools. When active, it can use any tool Claude has access to.
LICENSE.txtassets/templates/background.tsassets/templates/content-script/main.tsassets/templates/package.jsonassets/templates/postcss.config.jsassets/templates/sidepanel/App.tsxassets/templates/sidepanel/index.htmlassets/templates/sidepanel/main.tsxassets/templates/sidepanel/style.cssassets/templates/tailwind.config.jsassets/templates/tsconfig.jsonassets/templates/wxt.config.tsreferences/messaging.mdreferences/mv3-permissions.mdreferences/wxt-config.mdScaffold production-ready Chrome MV3 extensions using WXT + React + shadcn-UI.
pnpm dlx wxt@latest init <project-name> --template react
cd <project-name>
pnpm install
pnpm dev
Ask user about extension type and features:
| Component | Purpose | When to Include |
|---|---|---|
| Background | Service worker, messaging hub, native messaging | Always |
| Content Script | DOM manipulation, page extraction | Interacting with web pages |
| Side Panel | Persistent UI alongside pages | Complex UIs, suggestion panels |
| Popup | Quick actions, settings access | Simple interactions |
| Options Page | Extension configuration | User preferences |
| DevTools Panel | Developer debugging tools | Development tooling |
/extension
├── entrypoints/
│ ├── background.ts # Service worker
│ ├── popup/ # Popup UI (optional)
│ │ ├── index.html
│ │ ├── App.tsx
│ │ └── style.css
│ ├── sidepanel/ # Side panel UI (optional)
│ │ ├── index.html
│ │ ├── App.tsx
│ │ └── style.css
│ └── options/ # Options page (optional)
│ ├── index.html
│ └── App.tsx
├── content/ # Content scripts
│ ├── main.ts # Primary content script
│ └── [site-name].ts # Site-specific scripts
├── lib/ # Shared utilities
│ ├── storage.ts # Storage wrapper
│ ├── messaging.ts # Message protocol
│ └── types.ts # Shared types
├── components/ # React components (shadcn-ui)
│ └── ui/
├── wxt.config.ts # WXT configuration
├── tailwind.config.js # Tailwind CSS
├── package.json
└── tsconfig.json
wxt.config.ts:
import { defineConfig } from 'wxt';
export default defineConfig({
modules: ['@wxt-dev/module-react'],
manifest: {
name: 'Extension Name',
version: '0.1.0',
permissions: ['storage', 'activeTab', 'scripting'],
host_permissions: ['https://example.com/*'],
},
});
See WXT Configuration Reference for complete options.
// entrypoints/background.ts
export default defineBackground(() => {
console.log('Extension loaded');
// Message handler
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'GET_DATA') {
// Handle message
sendResponse({ success: true, data: {} });
}
return true; // Keep channel open for async response
});
});
// content/main.ts
export default defineContentScript({
matches: ['https://example.com/*'],
main(ctx) {
console.log('Content script loaded on', window.location.href);
// Extract page data
const pageData = extractPageContent();
// Send to background
browser.runtime.sendMessage({ type: 'PAGE_DATA', data: pageData });
},
});
// entrypoints/sidepanel/App.tsx
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
export default function App() {
const [data, setData] = useState<DataType[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
// Listen for messages from background
browser.runtime.onMessage.addListener((message) => {
if (message.type === 'NEW_DATA') {
setData(prev => [message.data, ...prev]);
}
});
}, []);
const handleAction = async () => {
setLoading(true);
const response = await browser.runtime.sendMessage({ type: 'RUN_ACTION' });
setLoading(false);
};
return (
<div className="p-4">
<Button onClick={handleAction} disabled={loading}>
{loading ? 'Processing...' : 'Run Action'}
</Button>
{data.map((item) => (
<Card key={item.id} className="mt-2 p-3">
{item.content}
</Card>
))}
</div>
);
}
// lib/storage.ts
import { storage } from 'wxt/storage';
export interface DocState {
docId: string;
title: string;
lastRunAt: number;
items: Item[];
dismissedIds: string[];
}
const docStateKey = (id: string) => `local:doc:${id}` as const;
export const docStorage = {
async get(docId: string): Promise<DocState | null> {
return storage.getItem<DocState>(docStateKey(docId));
},
async set(docId: string, state: DocState): Promise<void> {
await storage.setItem(docStateKey(docId), state);
},
watch(docId: string, callback: (state: DocState | null) => void) {
return storage.watch<DocState>(docStateKey(docId), callback);
},
};
// lib/messaging.ts
export const PROTOCOL_VERSION = '1.0.0';
export type MessageType =
| { type: 'DOC_OPEN'; doc: DocPayload }
| { type: 'DOC_CHUNK'; docId: string; chunk: string; index: number }
| { type: 'DOC_DONE'; docId: string }
| { type: 'SUGGESTIONS'; docId: string; items: Suggestion[] }
| { type: 'INSERT_FIX'; suggestionId: string }
| { type: 'ERROR'; code: string; message: string };
export async function sendMessage<T extends MessageType>(
message: T
): Promise<MessageResponse<T>> {
return browser.runtime.sendMessage({ ...message, protocolVersion: PROTOCOL_VERSION });
}
// lib/nativeAdapter.ts
export interface NativeAdapter {
connect(): Promise<void>;
send(message: unknown): void;
onMessage(callback: (msg: unknown) => void): void;
disconnect(): void;
}
export function createNativeAdapter(appName: string): NativeAdapter {
let port: browser.Runtime.Port | null = null;
return {
connect() {
port = browser.runtime.connectNative(appName);
return Promise.resolve();
},
send(message) {
port?.postMessage(message);
},
onMessage(callback) {
port?.onMessage.addListener(callback);
},
disconnect() {
port?.disconnect();
port = null;
},
};
}
// Mock adapter for development
export function createMockAdapter(): NativeAdapter {
return {
async connect() {},
send(message) {
// Simulate response after delay
setTimeout(() => {
// Return mock data
}, 1000);
},
onMessage(callback) {},
disconnect() {},
};
}
// content/insert.ts
export function insertText(text: string): boolean {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return false;
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(text));
// Collapse selection to end
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
return true;
}
// For contenteditable elements (Google Docs workaround)
export async function insertViaClipboard(text: string): Promise<boolean> {
try {
await navigator.clipboard.writeText(text);
document.execCommand('paste');
return true;
} catch {
return false;
}
}
// content/overlay.ts
import { createShadowRootUi } from 'wxt/content-script-ui/shadow-root';
import { createRoot } from 'react-dom/client';
import Overlay from './Overlay';
export default defineContentScript({
matches: ['https://example.com/*'],
cssInjectionMode: 'ui',
async main(ctx) {
const ui = await createShadowRootUi(ctx, {
name: 'my-overlay',
position: 'inline',
anchor: 'body',
onMount: (container) => {
const root = createRoot(container);
root.render(<Overlay />);
return root;
},
onRemove: (root) => {
root?.unmount();
},
});
ui.mount();
},
});
// content/gdocs.ts
export default defineContentScript({
matches: ['https://docs.google.com/document/*'],
main(ctx) {
const docId = extractDocId(window.location.href);
const title = document.title.replace(' - Google Docs', '');
// Google Docs renders text in .kix-lineview elements
const lines = document.querySelectorAll('.kix-lineview');
const text = Array.from(lines)
.map(el => el.textContent || '')
.join('\n');
const cursorContext = getCursorContext();
const headings = extractHeadings();
browser.runtime.sendMessage({
type: 'DOC_OPEN',
doc: { docId, title, text, cursorContext, headings },
});
},
});
function extractDocId(url: string): string {
const match = url.match(/\/document\/d\/([a-zA-Z0-9-_]+)/);
return match?.[1] || '';
}
function getCursorContext(): { before: string; after: string } {
// Implementation depends on Google Docs DOM structure
return { before: '', after: '' };
}
function extractHeadings(): { text: string; start: number }[] {
// Extract heading elements
return [];
}
// content/overleaf.ts
export default defineContentScript({
matches: ['https://www.overleaf.com/project/*'],
main(ctx) {
const projectId = extractProjectId(window.location.href);
// Overleaf uses CodeMirror - access editor content
const editor = document.querySelector('.cm-content');
const text = editor?.textContent || '';
browser.runtime.sendMessage({
type: 'DOC_OPEN',
doc: { docId: projectId, title: document.title, text },
});
},
});
function extractProjectId(url: string): string {
const match = url.match(/\/project\/([a-f0-9]+)/);
return match?.[1] || '';
}
# After WXT init
pnpm add -D tailwindcss postcss autoprefixer
pnpm dlx tailwindcss init -p
# Add shadcn-ui
pnpm dlx shadcn@latest init
pnpm dlx shadcn@latest add button card toast badge
tailwind.config.js:
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ['class'],
content: [
'./entrypoints/**/*.{ts,tsx,html}',
'./components/**/*.{ts,tsx}',
],
theme: {
extend: {},
},
plugins: [],
};
// lib/__tests__/storage.test.ts
import { describe, it, expect, vi } from 'vitest';
import { docStorage } from '../storage';
vi.mock('wxt/storage', () => ({
storage: {
getItem: vi.fn(),
setItem: vi.fn(),
},
}));
describe('docStorage', () => {
it('should get doc state by id', async () => {
const state = await docStorage.get('doc-123');
expect(state).toBeDefined();
});
});
// e2e/extension.spec.ts
import { test, expect, chromium } from '@playwright/test';
test('extension loads side panel', async () => {
const pathToExtension = './dist/chrome-mv3';
const context = await chromium.launchPersistentContext('', {
headless: false,
args: [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
],
});
const page = await context.newPage();
await page.goto('https://docs.google.com/document/d/test');
// Test content script injection
// Test side panel interaction
});
{
"scripts": {
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"test": "vitest",
"lint": "eslint ."
}
}
Environment Variables:
# .env.development
USE_MOCK_NATIVE=true
# .env.production
USE_MOCK_NATIVE=false
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.