Build an AI Chrome Extension: Add Claude to Any Webpage in 60 Minutes
Build a Chrome extension that summarizes web pages, answers questions about content, and rewrites selected text — all powered by Claude. Full source code and step-by-step instructions included.
Chrome extensions are the most underrated distribution channel for AI tools. They have zero onboarding friction — install and it works on every webpage. No new tabs, no copy-pasting between apps, no context switching.
In this tutorial, you’ll build a Chrome extension that:
- Summarizes any webpage with one click
- Answers questions about the current page’s content
- Rewrites selected text in different tones
- Works via a clean sidebar UI
We’re using Manifest V3 (required for all new Chrome extensions) and Claude’s API for the AI backend.
Project Structure
ai-chrome-extension/
├── manifest.json # Extension configuration
├── background.js # Service worker (API calls)
├── content.js # Content script (page interaction)
├── sidebar/
│ ├── sidebar.html # Sidebar UI
│ ├── sidebar.css # Styling
│ └── sidebar.js # Sidebar logic
├── popup/
│ ├── popup.html # Settings popup
│ └── popup.js # Settings logic
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
Step 1: The Manifest
{
"manifest_version": 3,
"name": "AI Page Assistant",
"version": "1.0.0",
"description": "Summarize pages, answer questions, and rewrite text with AI",
"permissions": [
"activeTab",
"storage",
"sidePanel",
"contextMenus"
],
"host_permissions": [
"https://api.anthropic.com/*"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"css": []
}
],
"side_panel": {
"default_path": "sidebar/sidebar.html"
},
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
Step 2: Background Service Worker
The background script handles API calls to Claude. API keys should never be in content scripts (which run in the page context).
// background.js
// Open side panel when extension icon is clicked
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })
.catch(console.error);
// Context menu for text selection
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: "rewrite-professional",
title: "Rewrite: Professional tone",
contexts: ["selection"]
});
chrome.contextMenus.create({
id: "rewrite-casual",
title: "Rewrite: Casual tone",
contexts: ["selection"]
});
chrome.contextMenus.create({
id: "rewrite-concise",
title: "Rewrite: Make concise",
contexts: ["selection"]
});
});
// Handle context menu clicks
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
const selectedText = info.selectionText;
if (!selectedText) return;
const toneMap = {
"rewrite-professional": "Rewrite this text in a professional, formal tone",
"rewrite-casual": "Rewrite this text in a casual, friendly tone",
"rewrite-concise": "Make this text more concise while keeping the key points"
};
const prompt = toneMap[info.menuItemId];
if (!prompt) return;
try {
const result = await callClaude(
`${prompt}:\n\n"${selectedText}"`,
"You are a writing assistant. Only output the rewritten text, nothing else."
);
// Send result to the side panel
chrome.runtime.sendMessage({
type: "rewrite-result",
original: selectedText,
rewritten: result
});
} catch (error) {
console.error("Rewrite error:", error);
}
});
// Handle messages from sidebar and content scripts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "summarize") {
handleSummarize(message.content).then(sendResponse);
return true; // Keep message channel open for async response
}
if (message.type === "ask-question") {
handleQuestion(message.content, message.question).then(sendResponse);
return true;
}
if (message.type === "get-page-content") {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]) {
chrome.tabs.sendMessage(tabs[0].id, { type: "extract-content" }, sendResponse);
}
});
return true;
}
});
async function handleSummarize(content) {
const truncated = content.substring(0, 12000);
return await callClaude(
`Summarize the following webpage content. Provide:\n` +
`1. A one-sentence TL;DR\n` +
`2. 3-5 key points as bullet points\n` +
`3. Any important data, statistics, or claims made\n\n` +
`Content:\n${truncated}`,
"You are a content summarizer. Be accurate and concise."
);
}
async function handleQuestion(content, question) {
const truncated = content.substring(0, 12000);
return await callClaude(
`Based on the following webpage content, answer this question:\n\n` +
`Question: ${question}\n\n` +
`Content:\n${truncated}`,
"Answer based only on the provided content. If the answer isn't in the content, say so."
);
}
async function callClaude(userMessage, systemPrompt) {
const { apiKey } = await chrome.storage.local.get("apiKey");
if (!apiKey) {
return "Please set your API key in the extension settings (click the extension icon).";
}
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2024-01-01",
"anthropic-dangerous-direct-browser-access": "true"
},
body: JSON.stringify({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
system: systemPrompt,
messages: [{ role: "user", content: userMessage }]
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || "API request failed");
}
const data = await response.json();
return data.content[0].text;
}
Step 3: Content Script
// content.js
// Extracts page content when requested by the background script
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "extract-content") {
const content = extractPageContent();
sendResponse({ content });
}
return true;
});
function extractPageContent() {
// Remove script, style, and nav elements
const clone = document.cloneNode(true);
const removeSelectors = [
"script", "style", "nav", "footer", "header",
"iframe", "noscript", ".ad", ".advertisement",
"[role='navigation']", "[role='banner']"
];
removeSelectors.forEach(selector => {
clone.querySelectorAll(selector).forEach(el => el.remove());
});
// Try to find main content
const main = clone.querySelector(
"main, article, [role='main'], .post-content, .article-content, .entry-content"
);
const textSource = main || clone.body;
// Extract clean text
const text = textSource.innerText
.replace(/\s+/g, " ")
.replace(/\n{3,}/g, "\n\n")
.trim();
return text.substring(0, 15000); // Limit to ~3750 tokens
}
Step 4: Sidebar UI
<!-- sidebar/sidebar.html -->
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="sidebar.css">
</head>
<body>
<div class="container">
<h1>AI Assistant</h1>
<div class="section">
<button id="summarize-btn" class="btn primary">
Summarize This Page
</button>
</div>
<div class="section">
<div class="input-group">
<input
type="text"
id="question-input"
placeholder="Ask a question about this page..."
>
<button id="ask-btn" class="btn">Ask</button>
</div>
</div>
<div id="loading" class="loading hidden">
<div class="spinner"></div>
<span>Thinking...</span>
</div>
<div id="result" class="result hidden">
<div class="result-header">
<span id="result-label">Result</span>
<button id="copy-btn" class="btn-small">Copy</button>
</div>
<div id="result-content"></div>
</div>
</div>
<script src="sidebar.js"></script>
</body>
</html>
/* sidebar/sidebar.css */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 14px;
color: #1a1a1a;
background: #fafafa;
}
.container { padding: 16px; }
h1 {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
color: #111;
}
.section { margin-bottom: 12px; }
.btn {
padding: 10px 16px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background 0.2s;
}
.btn.primary {
background: #2563eb;
color: white;
width: 100%;
}
.btn.primary:hover { background: #1d4ed8; }
.btn:not(.primary) {
background: #e5e7eb;
color: #374151;
}
.input-group {
display: flex;
gap: 8px;
}
input {
flex: 1;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
outline: none;
}
input:focus { border-color: #2563eb; }
.loading {
text-align: center;
padding: 20px;
color: #6b7280;
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid #e5e7eb;
border-top: 3px solid #2563eb;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 8px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.result {
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
margin-top: 12px;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
#result-label {
font-weight: 600;
font-size: 13px;
color: #6b7280;
text-transform: uppercase;
}
.btn-small {
padding: 4px 10px;
font-size: 12px;
border: 1px solid #d1d5db;
background: white;
border-radius: 4px;
cursor: pointer;
}
#result-content {
line-height: 1.6;
white-space: pre-wrap;
}
.hidden { display: none; }
// sidebar/sidebar.js
const summarizeBtn = document.getElementById("summarize-btn");
const askBtn = document.getElementById("ask-btn");
const questionInput = document.getElementById("question-input");
const loading = document.getElementById("loading");
const result = document.getElementById("result");
const resultContent = document.getElementById("result-content");
const resultLabel = document.getElementById("result-label");
const copyBtn = document.getElementById("copy-btn");
function showLoading() {
loading.classList.remove("hidden");
result.classList.add("hidden");
}
function showResult(text, label = "Result") {
loading.classList.add("hidden");
result.classList.remove("hidden");
resultContent.textContent = text;
resultLabel.textContent = label;
}
function hideLoading() {
loading.classList.add("hidden");
}
// Get page content helper
async function getPageContent() {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ type: "get-page-content" }, (response) => {
resolve(response?.content || "Could not extract page content.");
});
});
}
// Summarize button
summarizeBtn.addEventListener("click", async () => {
showLoading();
try {
const content = await getPageContent();
chrome.runtime.sendMessage(
{ type: "summarize", content },
(response) => {
showResult(response, "Summary");
}
);
} catch (error) {
showResult("Error: " + error.message, "Error");
}
});
// Ask question
askBtn.addEventListener("click", async () => {
const question = questionInput.value.trim();
if (!question) return;
showLoading();
try {
const content = await getPageContent();
chrome.runtime.sendMessage(
{ type: "ask-question", content, question },
(response) => {
showResult(response, "Answer");
questionInput.value = "";
}
);
} catch (error) {
showResult("Error: " + error.message, "Error");
}
});
// Enter key to submit question
questionInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") askBtn.click();
});
// Copy button
copyBtn.addEventListener("click", () => {
navigator.clipboard.writeText(resultContent.textContent);
copyBtn.textContent = "Copied!";
setTimeout(() => { copyBtn.textContent = "Copy"; }, 2000);
});
// Listen for rewrite results from context menu
chrome.runtime.onMessage.addListener((message) => {
if (message.type === "rewrite-result") {
showResult(message.rewritten, "Rewritten");
}
});
Step 5: Settings Popup
<!-- popup/popup.html -->
<!DOCTYPE html>
<html>
<head>
<style>
body { width: 300px; padding: 16px; font-family: system-ui; }
h2 { font-size: 16px; margin-bottom: 12px; }
label { font-size: 13px; color: #555; display: block; margin-bottom: 4px; }
input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 6px; margin-bottom: 12px; }
button { width: 100%; padding: 8px; background: #2563eb; color: white; border: none; border-radius: 6px; cursor: pointer; }
.status { font-size: 12px; color: #22c55e; margin-top: 8px; display: none; }
</style>
</head>
<body>
<h2>AI Page Assistant Settings</h2>
<label>Anthropic API Key</label>
<input type="password" id="api-key" placeholder="sk-ant-...">
<button id="save">Save</button>
<div class="status" id="status">Settings saved!</div>
<script src="popup.js"></script>
</body>
</html>
// popup/popup.js
const apiKeyInput = document.getElementById("api-key");
const saveBtn = document.getElementById("save");
const status = document.getElementById("status");
// Load saved key
chrome.storage.local.get("apiKey", ({ apiKey }) => {
if (apiKey) apiKeyInput.value = apiKey;
});
saveBtn.addEventListener("click", () => {
const apiKey = apiKeyInput.value.trim();
chrome.storage.local.set({ apiKey }, () => {
status.style.display = "block";
setTimeout(() => { status.style.display = "none"; }, 2000);
});
});
Step 6: Load and Test
- Open Chrome and navigate to
chrome://extensions/ - Enable “Developer mode” (top right toggle)
- Click “Load unpacked”
- Select your project folder
- Click the extension icon and enter your Anthropic API key
- Navigate to any webpage
- Click the extension icon to open the sidebar
- Click “Summarize This Page” or ask a question
To test the context menu: select text on any page, right-click, and choose a rewrite option.
Publishing to Chrome Web Store
When you’re ready to publish:
# Create a ZIP of your extension
cd ai-chrome-extension
zip -r extension.zip . -x ".*" -x "__MACOSX"
- Go to the Chrome Developer Dashboard
- Pay the one-time $5 registration fee
- Upload your ZIP file
- Fill in listing details, screenshots, and privacy policy
- Submit for review (typically 1-3 business days)
Privacy policy requirement: Since your extension sends page content to an external API, you need a privacy policy explaining what data is collected and how it’s used.
Cost and Performance
Typical usage:
- Page summary: ~3,000 input tokens + ~500 output tokens
- Question: ~3,000 input tokens + ~200 output tokens
- Rewrite: ~200 input tokens + ~200 output tokens
Using Claude Sonnet at current pricing:
- Summary: ~$0.012 per page
- Question: ~$0.012 per question
- Rewrite: ~$0.004 per rewrite
100 summaries/month = ~$1.20
That’s the beauty of building your own AI tools: you pay API cost, not subscription price. A commercial version of this extension would charge $15-20/month. Your version costs $1-2/month in API calls.
The extension you just built in 60 minutes does 80% of what paid AI browser extensions offer. The remaining 20% — polished UI, error handling edge cases, multi-language support — is where you can iterate and improve. But the core functionality? It works right now.
Sources
> Want more like this?
Get the best AI insights delivered weekly.
> Related Articles
Web Scraping with AI: Build a Smart Data Extraction Pipeline
Traditional web scraping breaks when websites change layouts. AI-powered scraping understands page structure and extracts data intelligently. Here's how to build one using Python, Beautiful Soup, and Claude.
Create an AI Art Portfolio: From Generation to Gallery in One Weekend
Build a professional AI art portfolio website with curated collections, consistent style, and proper attribution. Covers prompt engineering, style consistency, curation, and deployment.
Create an AI Content Moderator: Automate Trust and Safety at Scale
Build a content moderation system that classifies text, images, and user reports with AI. Production patterns for trust and safety.
Tags
> Stay in the loop
Weekly AI tools & insights.