TUTORIALS 13 min read

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.

By EgoistAI ·
Build an AI Chrome Extension: Add Claude to Any Webpage in 60 Minutes

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

  1. Open Chrome and navigate to chrome://extensions/
  2. Enable “Developer mode” (top right toggle)
  3. Click “Load unpacked”
  4. Select your project folder
  5. Click the extension icon and enter your Anthropic API key
  6. Navigate to any webpage
  7. Click the extension icon to open the sidebar
  8. 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"
  1. Go to the Chrome Developer Dashboard
  2. Pay the one-time $5 registration fee
  3. Upload your ZIP file
  4. Fill in listing details, screenshots, and privacy policy
  5. 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.

Share this article

> Want more like this?

Get the best AI insights delivered weekly.

> Related Articles

Tags

Chrome extensionJavaScriptClaude APIweb developmenttutorial

> Stay in the loop

Weekly AI tools & insights.