Build Chrome extensions with React, TypeScript, Tailwind CSS, and shadcn/ui. Use when users need to create browser extensions, content scripts, or popup UIs. Covers Manifest V3, messaging, storage, and permissions.
/plugin marketplace add leobrival/topographic-studio-plugins/plugin install code-workflows@topographic-studio-pluginsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
assets/background/service-worker.tsassets/content/content.tsxassets/lib/messaging.tsassets/lib/storage.tsassets/popup/Popup.tsxassets/popup/index.tsxreferences/component-patterns.mdreferences/manifest-v3.mdreferences/publishing.mdModern Chrome extensions with React, Tailwind CSS, and shadcn/ui.
User request → What type of extension?
│
├─ Popup UI
│ ├─ React → Component-based UI
│ ├─ Tailwind → Utility-first styling
│ └─ shadcn/ui → Beautiful components
│
├─ Content Script
│ ├─ DOM manipulation → Modify pages
│ ├─ Inject UI → Add elements
│ └─ Intercept → Network requests
│
├─ Background Script
│ ├─ Service Worker → Event-driven
│ ├─ Storage → Persist data
│ └─ Alarms → Scheduled tasks
│
└─ Features
├─ Messaging → Component communication
├─ Storage → sync/local
├─ Permissions → API access
└─ Context Menu → Right-click actions
# Create project with Vite
pnpm create vite my-extension --template react-ts
cd my-extension
# Install dependencies
pnpm add -D @crxjs/vite-plugin
pnpm add -D tailwindcss postcss autoprefixer
pnpm add -D @biomejs/biome
# shadcn/ui setup
pnpm dlx shadcn@latest init
pnpm dlx shadcn@latest add button card input
my-extension/
├── src/
│ ├── popup/
│ │ ├── Popup.tsx
│ │ ├── index.tsx
│ │ └── index.html
│ ├── content/
│ │ └── content.tsx
│ ├── background/
│ │ └── service-worker.ts
│ ├── components/
│ │ └── ui/
│ ├── lib/
│ │ ├── storage.ts
│ │ └── messaging.ts
│ └── styles/
│ └── globals.css
├── public/
│ └── icons/
│ ├── icon16.png
│ ├── icon48.png
│ └── icon128.png
├── manifest.json
├── vite.config.ts
├── tailwind.config.js
└── biome.json
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0.0",
"description": "A beautiful Chrome extension",
"action": {
"default_popup": "src/popup/index.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"background": {
"service_worker": "src/background/service-worker.ts",
"type": "module"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["src/content/content.tsx"],
"css": ["src/styles/content.css"]
}
],
"permissions": [
"storage",
"activeTab",
"scripting"
],
"host_permissions": [
"https://*/*"
],
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";
import path from "path";
export default defineConfig({
plugins: [react(), crx({ manifest })],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
build: {
rollupOptions: {
input: {
popup: "src/popup/index.html",
},
},
},
});
// src/popup/Popup.tsx
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { storage } from "@/lib/storage";
export function Popup() {
const [apiKey, setApiKey] = useState("");
const [saved, setSaved] = useState(false);
useEffect(() => {
storage.get("apiKey").then((key) => {
if (key) setApiKey(key);
});
}, []);
const handleSave = async () => {
await storage.set("apiKey", apiKey);
setSaved(true);
setTimeout(() => setSaved(false), 2000);
};
return (
<div className="w-80 p-4">
<Card>
<CardHeader>
<CardTitle className="text-lg">My Extension</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">API Key</label>
<Input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="Enter your API key"
/>
</div>
<Button onClick={handleSave} className="w-full">
{saved ? "Saved!" : "Save Settings"}
</Button>
</CardContent>
</Card>
</div>
);
}
// src/lib/storage.ts
type StorageArea = "sync" | "local";
class Storage {
private area: StorageArea;
constructor(area: StorageArea = "sync") {
this.area = area;
}
async get<T>(key: string): Promise<T | undefined> {
const result = await chrome.storage[this.area].get(key);
return result[key];
}
async set<T>(key: string, value: T): Promise<void> {
await chrome.storage[this.area].set({ [key]: value });
}
async remove(key: string): Promise<void> {
await chrome.storage[this.area].remove(key);
}
async clear(): Promise<void> {
await chrome.storage[this.area].clear();
}
onChange(callback: (changes: { [key: string]: chrome.storage.StorageChange }) => void) {
chrome.storage.onChanged.addListener((changes, area) => {
if (area === this.area) {
callback(changes);
}
});
}
}
export const storage = new Storage("sync");
export const localStorage = new Storage("local");
// src/lib/messaging.ts
type MessageType = "GET_PAGE_DATA" | "PROCESS_SELECTION" | "UPDATE_BADGE";
interface Message<T = unknown> {
type: MessageType;
payload?: T;
}
// Send from popup/content to background
export async function sendMessage<T, R>(type: MessageType, payload?: T): Promise<R> {
return chrome.runtime.sendMessage({ type, payload });
}
// Send to content script
export async function sendToTab<T, R>(tabId: number, type: MessageType, payload?: T): Promise<R> {
return chrome.tabs.sendMessage(tabId, { type, payload });
}
// Listen for messages (in background/content)
export function onMessage<T, R>(
handler: (message: Message<T>, sender: chrome.runtime.MessageSender) => Promise<R> | R
) {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const result = handler(message, sender);
if (result instanceof Promise) {
result.then(sendResponse);
return true; // Keep channel open for async response
}
sendResponse(result);
return false;
});
}
// src/background/service-worker.ts
import { onMessage } from "@/lib/messaging";
// Handle installation
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === "install") {
console.log("Extension installed");
// Set default settings
chrome.storage.sync.set({ enabled: true });
}
});
// Handle messages
onMessage(async (message, sender) => {
switch (message.type) {
case "GET_PAGE_DATA":
// Process and return data
return { success: true, data: "processed" };
case "UPDATE_BADGE":
chrome.action.setBadgeText({ text: String(message.payload) });
chrome.action.setBadgeBackgroundColor({ color: "#22c55e" });
return { success: true };
default:
return { success: false, error: "Unknown message type" };
}
});
// Context menu
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: "processSelection",
title: "Process with My Extension",
contexts: ["selection"],
});
});
chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === "processSelection" && info.selectionText) {
// Handle selection
console.log("Selected:", info.selectionText);
}
});
// src/content/content.tsx
import { createRoot } from "react-dom/client";
import { onMessage, sendMessage } from "@/lib/messaging";
// Inject React component into page
function injectUI() {
const container = document.createElement("div");
container.id = "my-extension-root";
document.body.appendChild(container);
const root = createRoot(container);
root.render(<ContentApp />);
}
function ContentApp() {
return (
<div className="fixed bottom-4 right-4 z-[9999]">
<button
onClick={() => sendMessage("UPDATE_BADGE", 1)}
className="bg-primary text-white px-4 py-2 rounded-lg shadow-lg"
>
Click me
</button>
</div>
);
}
// Listen for messages from popup/background
onMessage((message) => {
if (message.type === "GET_PAGE_DATA") {
return {
title: document.title,
url: window.location.href,
content: document.body.innerText.slice(0, 1000),
};
}
});
// Initialize
injectUI();
| Permission | Use Case |
|---|---|
storage | Save user settings |
activeTab | Access current tab |
scripting | Inject scripts |
tabs | Query all tabs |
contextMenus | Right-click menu |
notifications | Show alerts |
alarms | Scheduled tasks |
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 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 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.