Build an AI Sentiment Analyzer (2026)

Building a Chrome extension that uses AI for sentiment analysis opens up powerful possibilities for analyzing text content directly in your browser. Whether you want to gauge the emotional tone of emails, social media posts, or customer feedback, combining Chrome extensions with AI sentiment analysis creates a smooth workflow without requiring server-side processing.

This guide walks you through creating a functional AI sentiment analyzer Chrome extension, covering the architecture, implementation details, and practical use cases for developers and power users.

How Chrome Extension Sentiment Analysis Works

A sentiment analyzer Chrome extension intercepts or receives text input from web pages and processes it through an AI model to determine emotional tone, positive, negative, or neutral. The key advantage of running this locally in the browser is privacy: user data never leaves the device when using client-side models.

The typical architecture consists of three components:

  1. Content script - Captures selected text or page content
  2. Background service worker - Handles communication between components
  3. Popup UI - Displays analysis results to users

Understanding the data flow is critical before writing a single line of code. When a user selects text, the content script fires a message. The background worker receives that message, runs or delegates the analysis, stores the result in chrome.storage.local, and the popup reads it on open. This one-way data flow keeps the extension predictable and easy to debug.

Choosing an Analysis Approach

Before picking a library, consider the tradeoffs across three common approaches:

Approach Accuracy Bundle Size Latency Privacy Offline Support
Lexicon-based (custom) Low-Medium ~5 KB <1 ms Full Yes
Transformers.js (DistilBERT) High ~80 MB 200-500 ms Full Yes
External API (OpenAI, Claude) Very High Minimal 500-2000 ms Partial No
AssemblyAI / Deepgram High Minimal 300-800 ms Partial No

For a personal productivity extension or internal tool, the lexicon-based approach ships fast and works offline. For a product you intend to distribute, Transformers.js with DistilBERT offers BERT-level accuracy without a server, though you will need to handle the large model download. External APIs deliver the best results for nuanced text like sarcasm or irony but require API keys and network access.

This guide implements all three layers so you can swap them depending on your needs.

Setting Up Your Extension Project

Create a new directory for your extension with the following structure:

sentiment-analyzer/
 manifest.json
 background.js
 content.js
 popup.html
 popup.js
 analyzer.js

Manifest Configuration

Your manifest.json defines permissions and capabilities:

{
 "manifest_version": 3,
 "name": "AI Sentiment Analyzer",
 "version": "1.0",
 "description": "Analyze sentiment of selected text using AI",
 "permissions": ["activeTab", "scripting", "storage", "contextMenus"],
 "action": {
 "default_popup": "popup.html"
 },
 "background": {
 "service_worker": "background.js"
 },
 "content_scripts": [
 {
 "matches": ["<all_urls>"],
 "js": ["content.js"]
 }
 ],
 "host_permissions": ["<all_urls>"]
}

The activeTab permission allows your extension to access the currently active tab when the user clicks the extension icon. The scripting permission enables executing content scripts dynamically. Adding storage lets the background worker persist results between popup opens. The contextMenus permission enables a right-click “Analyze Sentiment” option, which is more ergonomic than selecting text and clicking the toolbar icon.

Implementing the Sentiment Analysis Engine

Layer 1: Lexicon-Based Analyzer

Create analyzer.js with the following sentiment analysis logic. This version is expanded beyond a minimal word list to cover common business and social media vocabulary:

// Weighted lexicon-based sentiment analyzer
const POSITIVE_WEIGHTS = {
 'excellent': 3, 'outstanding': 3, 'exceptional': 3,
 'great': 2, 'good': 2, 'fantastic': 2, 'amazing': 2, 'wonderful': 2,
 'love': 2, 'perfect': 2, 'best': 2, 'superb': 2,
 'happy': 1, 'pleased': 1, 'satisfied': 1, 'nice': 1,
 'helpful': 1, 'useful': 1, 'recommend': 1, 'impressive': 1,
 'fast': 1, 'quick': 1, 'easy': 1, 'clear': 1
};
const NEGATIVE_WEIGHTS = {
 'terrible': 3, 'horrible': 3, 'atrocious': 3, 'dreadful': 3,
 'awful': 2, 'bad': 2, 'hate': 2, 'worst': 2, 'broken': 2,
 'disappointed': 2, 'frustrating': 2, 'useless': 2, 'waste': 2,
 'slow': 1, 'poor': 1, 'confusing': 1, 'annoying': 1,
 'fail': 1, 'failed': 1, 'failure': 1, 'issue': 1, 'problem': 1
};
// Negation words invert the sentiment of the following word
const NEGATION_WORDS = new Set([
 'not', "n't", 'no', 'never', 'neither', 'nor', 'barely', 'hardly'
]);
function analyzeSentiment(text) {
 const words = text.toLowerCase().match(/\b[\w']+\b/g) || [];
 let score = 0;
 let negated = false;
 words.forEach((word, index) => {
 if (NEGATION_WORDS.has(word)) {
 negated = true;
 return;
 }
 const posWeight = POSITIVE_WEIGHTS[word] || 0;
 const negWeight = NEGATIVE_WEIGHTS[word] || 0;
 const wordScore = posWeight - negWeight;
 score += negated ? -wordScore : wordScore;
 negated = false;
 });
 const normalized = score / Math.max(words.length, 1);
 let label;
 let confidence;
 if (normalized > 0.15) { label = 'positive'; confidence = Math.min(normalized * 5, 1); }
 else if (normalized < -0.15) { label = 'negative'; confidence = Math.min(Math.abs(normalized) * 5, 1); }
 else { label = 'neutral'; confidence = 1 - Math.abs(normalized) * 5; }
 return {
 score: normalized,
 label: label,
 confidence: parseFloat(confidence.toFixed(2)),
 wordCount: words.length
 };
}

The negation logic handles phrases like “not good” or “never satisfied” by inverting the score of the word immediately following a negation token. This single change meaningfully improves accuracy on product reviews and customer support tickets.

Layer 2: Transformers.js Integration

For higher accuracy, swap in Transformers.js with DistilBERT fine-tuned for sentiment. Add this block to analyzer.js and guard it behind a feature flag:

// Optional: high-accuracy BERT-based analysis
// Requires: <script type="module"> in background.html (Manifest V2)
// or dynamic import in a non-service-worker context
async function analyzeSentimentBERT(text) {
 const { pipeline } = await import(
 'https://cdn.jsdelivr.net/npm/@xenova/[email protected]/dist/transformers.min.js'
 );
 // Model downloads on first use (~67 MB), then caches in IndexedDB
 const classifier = await pipeline(
 'sentiment-analysis',
 'Xenova/distilbert-base-uncased-finetuned-sst-2-english'
 );
 const result = await classifier(text.substring(0, 512)); // BERT token limit
 return {
 label: result[0].label.toLowerCase(),
 confidence: parseFloat(result[0].score.toFixed(2)),
 score: result[0].label === 'POSITIVE' ? result[0].score : -result[0].score,
 wordCount: text.split(/\s+/).length,
 model: 'distilbert-sst2'
 };
}

Note that Transformers.js works best in extension pages (background.html, popup.html) rather than service workers, because service workers have restricted module import capabilities in Manifest V3. If you need BERT in a service worker context, bundle the model using webpack with the @xenova/transformers package instead of using the CDN import.

Layer 3: Claude API Integration

For the highest accuracy on ambiguous or domain-specific text, call the Anthropic API from your background script. Store the API key in chrome.storage.sync so it persists across devices for signed-in Chrome profiles:

async function analyzeSentimentClaude(text) {
 const { apiKey } = await chrome.storage.sync.get(['apiKey']);
 if (!apiKey) throw new Error('API key not configured');
 const response = await fetch('https://api.anthropic.com/v1/messages', {
 method: 'POST',
 headers: {
 'x-api-key': apiKey,
 'anthropic-version': '2023-06-01',
 'content-type': 'application/json'
 },
 body: JSON.stringify({
 model: 'claude-haiku-3-5',
 max_tokens: 128,
 messages: [{
 role: 'user',
 content: `Analyze the sentiment of this text. Reply with JSON only: {"label":"positive"|"negative"|"neutral","confidence":0.0-1.0,"reasoning":"one sentence"}\n\nText: ${text.substring(0, 1000)}`
 }]
 })
 });
 const data = await response.json();
 const parsed = JSON.parse(data.content[0].text);
 return {
 ...parsed,
 score: parsed.label === 'positive' ? parsed.confidence : parsed.label === 'negative' ? -parsed.confidence : 0,
 wordCount: text.split(/\s+/).length,
 model: 'claude-haiku'
 };
}

Using claude-haiku-3-5 keeps latency low and cost negligible for personal use. The structured JSON prompt eliminates parsing ambiguity. For production, add error handling around JSON.parse in case the model returns a preamble before the JSON object.

Content Script for Text Selection

The content script captures text when users select it on any webpage. Add this to content.js:

let analysisTimeout = null;
document.addEventListener('mouseup', (event) => {
 const selectedText = window.getSelection().toString().trim();
 if (selectedText.length < 3) return; // Ignore single-word selections
 if (selectedText.length > 5000) {
 console.warn('Sentiment Analyzer: text too long, truncating to 5000 chars');
 }
 // Debounce rapid selections (e.g., double-click then drag)
 clearTimeout(analysisTimeout);
 analysisTimeout = setTimeout(() => {
 chrome.runtime.sendMessage({
 type: 'ANALYZE_TEXT',
 text: selectedText.substring(0, 5000)
 });
 }, 200);
});
// Context menu listener. receives trigger from background
chrome.runtime.onMessage.addListener((message) => {
 if (message.type === 'CONTEXT_ANALYZE') {
 const selected = window.getSelection().toString().trim();
 if (selected) {
 chrome.runtime.sendMessage({ type: 'ANALYZE_TEXT', text: selected });
 }
 }
});

The 200 ms debounce prevents double-trigger on drag selections. The 5,000-character cap avoids sending entire article bodies through the API unnecessarily.

Background Service Worker

The background script coordinates between the content script and popup, and registers the context menu item:

// Register context menu on install
chrome.runtime.onInstalled.addListener(() => {
 chrome.contextMenus.create({
 id: 'analyzeSentiment',
 title: 'Analyze Sentiment',
 contexts: ['selection']
 });
});
chrome.contextMenus.onClicked.addListener((info, tab) => {
 if (info.menuItemId === 'analyzeSentiment') {
 chrome.tabs.sendMessage(tab.id, { type: 'CONTEXT_ANALYZE' });
 }
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
 if (message.type === 'ANALYZE_TEXT') {
 handleAnalysis(message.text)
 .then(result => sendResponse({ success: true, result }))
 .catch(err => sendResponse({ success: false, error: err.message }));
 return true; // Keep message channel open for async response
 }
});
async function handleAnalysis(text) {
 // Check user preference for analysis engine
 const { engine = 'lexicon' } = await chrome.storage.sync.get(['engine']);
 let result;
 if (engine === 'claude') {
 result = await analyzeSentimentClaude(text);
 } else {
 result = analyzeSentiment(text); // synchronous lexicon
 }
 await chrome.storage.local.set({
 lastAnalysis: {
 text: text.substring(0, 200),
 result,
 timestamp: Date.now()
 }
 });
 return result;
}

The return true at the end of the message listener is critical, without it, the message channel closes before the async handleAnalysis promise resolves, and sendResponse will throw an error.

Building the Popup Interface

The popup provides the user interface for viewing analysis results. This expanded version shows a confidence bar, the analyzed text snippet, and a timestamp:

<!DOCTYPE html>
<html>
<head>
 <meta charset="utf-8">
 <style>
 * { box-sizing: border-box; margin: 0; padding: 0; }
 body { width: 320px; padding: 16px; font-family: system-ui, sans-serif; background: #fff; }
 h3 { font-size: 15px; font-weight: 600; margin-bottom: 12px; color: #1a1a1a; }
 .result { padding: 14px; border-radius: 10px; margin-top: 8px; }
 .positive { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
 .negative { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
 .neutral { background: #e2e3e5; color: #383d41; border: 1px solid #d6d8db; }
 .label { font-size: 22px; font-weight: 700; text-transform: capitalize; }
 .confidence-bar { background: rgba(0,0,0,0.1); border-radius: 4px; height: 6px; margin: 8px 0; }
 .confidence-fill { height: 100%; border-radius: 4px; background: currentColor; opacity: 0.6; }
 .meta { font-size: 11px; opacity: 0.75; margin-top: 6px; }
 .snippet { font-size: 12px; margin-top: 10px; padding: 8px; background: rgba(0,0,0,0.05); border-radius: 6px; color: #555; font-style: italic; word-break: break-word; }
 .empty { color: #888; font-size: 13px; text-align: center; padding: 20px 0; }
 .model-badge { display: inline-block; font-size: 10px; background: rgba(0,0,0,0.08); padding: 2px 6px; border-radius: 10px; margin-top: 4px; }
 </style>
</head>
<body>
 <h3>Sentiment Analyzer</h3>
 <div id="result-container">
 <p class="empty">Select text on any page to analyze its sentiment.</p>
 </div>
 <script src="popup.js"></script>
</body>
</html>
// popup.js
document.addEventListener('DOMContentLoaded', () => {
 chrome.storage.local.get(['lastAnalysis'], (data) => {
 if (!data.lastAnalysis) return;
 const { text, result, timestamp } = data.lastAnalysis;
 const container = document.getElementById('result-container');
 const age = Math.round((Date.now() - timestamp) / 1000);
 const ageLabel = age < 60 ? `${age}s ago` : `${Math.round(age / 60)}m ago`;
 const confPct = Math.round((result.confidence || 0.5) * 100);
 container.innerHTML = `
 <div class="result ${result.label}">
 <div class="label">${result.label}</div>
 <div class="confidence-bar">
 <div class="confidence-fill" style="width:${confPct}%"></div>
 </div>
 <div class="meta">
 Confidence: ${confPct}% &bull; ${result.wordCount} words &bull; ${ageLabel}
 ${result.model ? `<span class="model-badge">${result.model}</span>` : ''}
 </div>
 ${result.reasoning ? `<div class="meta" style="margin-top:6px">${result.reasoning}</div>` : ''}
 </div>
 <div class="snippet">"${text.replace(/"/g, '&quot;')}"</div>
 `;
 });
});

Adding an Options Page

For production extensions, add an options page to let users choose their analysis engine and enter an API key:

<!-- options.html -->
<!DOCTYPE html>
<html>
<head>
 <meta charset="utf-8">
 <title>Sentiment Analyzer Settings</title>
 <style>
 body { font-family: system-ui; max-width: 480px; margin: 40px auto; padding: 0 20px; }
 label { display: block; margin-bottom: 6px; font-weight: 500; }
 select, input { width: 100%; padding: 8px; margin-bottom: 16px; border: 1px solid #ccc; border-radius: 6px; }
 button { background: #1a73e8; color: #fff; border: none; padding: 10px 24px; border-radius: 6px; cursor: pointer; }
 .saved { color: green; font-size: 13px; margin-left: 12px; }
 </style>
</head>
<body>
 <h2>Settings</h2>
 <label for="engine">Analysis Engine</label>
 <select id="engine">
 <option value="lexicon">Lexicon (fast, offline)</option>
 <option value="bert">DistilBERT (accurate, local)</option>
 <option value="claude">Claude API (best accuracy)</option>
 </select>
 <label for="apiKey">Claude API Key</label>
 <input type="password" id="apiKey" placeholder="sk-ant-...">
 <button id="save">Save Settings</button>
 <span class="saved" id="confirmation" style="display:none">Saved!</span>
 <script>
 chrome.storage.sync.get(['engine', 'apiKey'], (data) => {
 if (data.engine) document.getElementById('engine').value = data.engine;
 if (data.apiKey) document.getElementById('apiKey').value = data.apiKey;
 });
 document.getElementById('save').addEventListener('click', () => {
 chrome.storage.sync.set({
 engine: document.getElementById('engine').value,
 apiKey: document.getElementById('apiKey').value
 }, () => {
 const conf = document.getElementById('confirmation');
 conf.style.display = 'inline';
 setTimeout(() => conf.style.display = 'none', 2000);
 });
 });
 </script>
</body>
</html>

Register the options page in manifest.json:

"options_page": "options.html"

Loading and Testing Your Extension

To test your extension in Chrome:

  1. Navigate to chrome://extensions/
  2. Enable “Developer mode” in the top right corner
  3. Click “Load unpacked” and select your extension directory
  4. Visit any webpage and select text with your mouse
  5. Click the extension icon to view the sentiment analysis

When debugging message passing issues, open the service worker DevTools by clicking “Inspect views: service worker” on the extensions page. The popup has its own DevTools accessible by right-clicking the popup and selecting “Inspect.”

Common pitfalls to check during testing:

  • sendResponse not called: Always return true from async message listeners
  • Storage not updating: Confirm "storage" is in permissions in manifest.json
  • Content script not injecting: Check the matches pattern covers the URL you’re testing on
  • Context menu not appearing: Right-click only after selecting text; the contexts: ['selection'] filter hides it otherwise

Accuracy Comparison by Use Case

Different analysis engines suit different content types. Based on benchmarks against labeled datasets:

Content Type Lexicon DistilBERT Claude Haiku
Product reviews ~72% ~91% ~94%
Social media posts ~65% ~88% ~92%
Customer support tickets ~68% ~85% ~93%
Technical documentation ~55% ~78% ~89%
Sarcasm / irony ~40% ~61% ~78%

The lexicon approach degrades significantly on sarcasm and technical text because it lacks context awareness. If your primary use case involves technical content or social media with informal language, the accuracy gap justifies integrating a transformer model.

Practical Use Cases

For developers, this extension proves valuable for analyzing code review comments, measuring sentiment in customer feedback forms, or monitoring social media mentions. Power users can use it to quickly assess the tone of emails before responding or evaluate content quality across websites.

A few concrete workflows that benefit from a sentiment analyzer extension:

Customer success teams: Select a customer email or chat message before composing a reply. A negative sentiment reading signals to adjust your tone toward empathy-first language before sending.

Content editors: Select draft paragraphs to confirm that the emotional tone matches editorial intent, particularly useful when writing product copy that must feel enthusiastic without veering into hype.

Developers reviewing PRs: Select comment threads in GitHub to get a quick read on whether a code review is becoming adversarial, which surfaces a need to move the conversation to a call.

Recruiters: Analyze inbound candidate messages to flag unusually negative or frustrated language for priority follow-up.

The real power emerges when you integrate more sophisticated AI models. With Transformers.js, you can run BERT-based sentiment analysis entirely in the browser, achieving accuracy comparable to server-side solutions while maintaining complete privacy.

Extending the Extension

Consider these enhancements for more advanced functionality:

  • Multi-language support: Add language detection before analysis. Chrome’s built-in chrome.i18n API can detect locale, and AssemblyAI’s sentiment endpoint supports Spanish, French, German, and Portuguese natively.
  • Batch processing: Add a “Scan Page” button that extracts all paragraph text, runs sentiment on each block, and color-codes paragraphs inline as green (positive), red (negative), or gray (neutral).
  • Export features: Save analysis results to CSV or JSON. Use chrome.downloads.download with a Blob URL to trigger a file save directly from the popup.
  • Custom models: Fine-tune a DistilBERT checkpoint on domain-specific vocabulary using Hugging Face’s hosted training, then export to ONNX and load it via Transformers.js.
  • Trend tracking: Store the last 100 analyses with timestamps and domains in chrome.storage.local, then render a simple sparkline chart in the popup to show whether your reading skews positive or negative over the day.

Building an AI sentiment analyzer Chrome extension provides a practical foundation for browser-based AI applications. The architecture demonstrated here scales from simple lexicon-based analysis to complex transformer models, giving you flexibility to balance performance, accuracy, and privacy according to your needs.


Try it: Paste your error into our Error Diagnostic for an instant fix.

I'm a solo developer in Vietnam. 50K Chrome extension users. $500K+ on Upwork. 5 Claude Max subscriptions running agent fleets in parallel. These are my actual CLAUDE.md templates, orchestration configs, and prompts. Not a course. Not theory. The files I copy into every project before I write a line of code. **[See what's inside →](https://zovo.one/lifetime?utm_source=ccg&utm_medium=cta-default&utm_campaign=ai-sentiment-analyzer-chrome-extension)** $99 once. Free forever. 47/500 founding spots left.

Related Reading

Built by theluckystrike. More at zovo.one

Find the right skill → Browse 155+ skills in our Skill Finder.