Inter-component messaging patterns for Chrome Extensions covering background ↔ content script ↔ popup ↔ side panel communication, ports, message passing, state synchronization, and error handling. Essential for multi-component extensions.
Implements Chrome extension messaging between background, content scripts, popup, and side panels. Claude uses this when building multi-component extensions that need to pass data or maintain state across isolated contexts.
/plugin marketplace add francanete/fran-marketplace/plugin install chrome-extension-expert@fran-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Chrome extensions have isolated components that must communicate via message passing:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Side Panel │ │ Popup │ │ Options Page │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└───────────────────────┼───────────────────────┘
│
┌────────────┴────────────┐
│ Background Service │
│ Worker │
└────────────┬────────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
┌────────┴────────┐ ┌────────┴────────┐ ┌────────┴────────┐
│ Content Script │ │ Content Script │ │ Content Script │
│ (Tab 1) │ │ (Tab 2) │ │ (Tab 3) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Content Script → Background:
// content.js
const response = await chrome.runtime.sendMessage({
type: 'SAVE_DATA',
data: { url: window.location.href }
});
console.log('Response:', response);
Background → Content Script:
// background.js
const response = await chrome.tabs.sendMessage(tabId, {
type: 'GET_PAGE_DATA'
});
Background Listener:
// background.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// sender.tab exists if from content script
if (sender.tab) {
console.log('From tab:', sender.tab.id, sender.tab.url);
}
switch (message.type) {
case 'SAVE_DATA':
saveData(message.data)
.then(result => sendResponse({ success: true, result }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true; // CRITICAL: Keep channel open for async response
case 'GET_STATUS':
sendResponse({ status: 'ready' }); // Sync response
break;
}
});
Popup/Side Panel → Background:
// popup.js or sidepanel.js
const response = await chrome.runtime.sendMessage({
type: 'FETCH_DATA',
query: 'search term'
});
updateUI(response.data);
Background → Popup/Side Panel:
// background.js - Broadcast to all extension pages
chrome.runtime.sendMessage({
type: 'DATA_UPDATED',
data: newData
}).catch(() => {
// No listeners - popup/panel might be closed
});
Use for streaming data or maintaining connection state.
Content Script → Background:
// content.js
const port = chrome.runtime.connect({ name: 'content-channel' });
port.onMessage.addListener((message) => {
console.log('Received:', message);
});
port.postMessage({ type: 'INIT', url: location.href });
// Handle disconnect
port.onDisconnect.addListener(() => {
console.log('Port disconnected');
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError.message);
}
});
Background Port Listener:
// background.js
const connectedPorts = new Map();
chrome.runtime.onConnect.addListener((port) => {
console.log('New connection:', port.name);
if (port.name === 'content-channel') {
const tabId = port.sender.tab.id;
connectedPorts.set(tabId, port);
port.onMessage.addListener((message) => {
handleContentMessage(message, port, tabId);
});
port.onDisconnect.addListener(() => {
connectedPorts.delete(tabId);
});
}
});
// Send to specific tab's content script
function sendToTab(tabId, message) {
const port = connectedPorts.get(tabId);
if (port) {
port.postMessage(message);
}
}
// sidepanel.js
let backgroundPort;
function connectToBackground() {
backgroundPort = chrome.runtime.connect({ name: 'sidepanel' });
backgroundPort.onMessage.addListener((message) => {
switch (message.type) {
case 'UPDATE':
updateUI(message.data);
break;
case 'STATUS':
showStatus(message.status);
break;
}
});
backgroundPort.onDisconnect.addListener(() => {
// Reconnect after delay
setTimeout(connectToBackground, 1000);
});
}
document.addEventListener('DOMContentLoaded', connectToBackground);
Content scripts and side panel cannot communicate directly. Use background as relay.
// content.js
chrome.runtime.sendMessage({
type: 'TO_SIDEPANEL',
payload: { selectedText: selection }
});
// background.js
const sidePanelPorts = new Map();
chrome.runtime.onConnect.addListener((port) => {
if (port.name === 'sidepanel') {
const windowId = port.sender.tab?.windowId;
sidePanelPorts.set(windowId, port);
port.onDisconnect.addListener(() => {
sidePanelPorts.delete(windowId);
});
}
});
chrome.runtime.onMessage.addListener((message, sender) => {
if (message.type === 'TO_SIDEPANEL') {
const windowId = sender.tab.windowId;
const sidePanelPort = sidePanelPorts.get(windowId);
if (sidePanelPort) {
sidePanelPort.postMessage({
type: 'FROM_CONTENT',
tabId: sender.tab.id,
payload: message.payload
});
}
}
});
// sidepanel.js
const port = chrome.runtime.connect({ name: 'sidepanel' });
port.onMessage.addListener((message) => {
if (message.type === 'FROM_CONTENT') {
handleContentData(message.payload, message.tabId);
}
});
// background.js
async function broadcastToAllTabs(message) {
const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
try {
await chrome.tabs.sendMessage(tab.id, message);
} catch (error) {
// Tab doesn't have content script
}
}
}
// Broadcast to tabs matching pattern
async function broadcastToMatchingTabs(pattern, message) {
const tabs = await chrome.tabs.query({ url: pattern });
const results = await Promise.allSettled(
tabs.map(tab => chrome.tabs.sendMessage(tab.id, message))
);
return results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
}
// Any component - update state
async function updateSharedState(updates) {
const { state } = await chrome.storage.local.get('state');
const newState = { ...state, ...updates };
await chrome.storage.local.set({ state: newState });
}
// Any component - listen for changes
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName === 'local' && changes.state) {
const newState = changes.state.newValue;
updateUI(newState);
}
});
// background.js
let appState = {
user: null,
data: [],
settings: {}
};
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
switch (message.type) {
case 'GET_STATE':
sendResponse({ state: appState });
break;
case 'UPDATE_STATE':
appState = { ...appState, ...message.updates };
// Notify all listeners
broadcastStateUpdate(appState);
sendResponse({ success: true, state: appState });
break;
case 'SUBSCRIBE':
// Add to subscribers list
break;
}
return true;
});
function broadcastStateUpdate(state) {
chrome.runtime.sendMessage({
type: 'STATE_CHANGED',
state
}).catch(() => {});
}
"Receiving end does not exist"
async function safeSendToTab(tabId, message) {
try {
return await chrome.tabs.sendMessage(tabId, message);
} catch (error) {
if (error.message.includes('Receiving end does not exist')) {
// Inject content script first
await chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
});
// Retry
return await chrome.tabs.sendMessage(tabId, message);
}
throw error;
}
}
"Extension context invalidated"
// content.js
function isContextValid() {
try {
chrome.runtime.id;
return true;
} catch {
return false;
}
}
async function safeSendMessage(message) {
if (!isContextValid()) {
console.warn('Extension context invalidated - reload page');
return null;
}
return chrome.runtime.sendMessage(message);
}
"Could not establish connection"
// With retry logic
async function sendWithRetry(message, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await chrome.runtime.sendMessage(message);
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(r => setTimeout(r, 100 * (i + 1)));
}
}
}
interface Message {
type: string;
payload?: any;
requestId?: string; // For request/response tracking
timestamp?: number;
}
interface Response {
success: boolean;
data?: any;
error?: string;
requestId?: string;
}
// messages.js - Shared message types
const MessageTypes = {
// Content → Background
SAVE_SELECTION: 'SAVE_SELECTION',
GET_PAGE_DATA: 'GET_PAGE_DATA',
// Background → Content
HIGHLIGHT_TEXT: 'HIGHLIGHT_TEXT',
UPDATE_CONFIG: 'UPDATE_CONFIG',
// Background → UI (Popup/SidePanel)
DATA_UPDATED: 'DATA_UPDATED',
STATUS_CHANGED: 'STATUS_CHANGED'
};
// Create message helper
function createMessage(type, payload = null) {
return {
type,
payload,
requestId: crypto.randomUUID(),
timestamp: Date.now()
};
}
manifest.json:
{
"externally_connectable": {
"matches": ["https://mywebsite.com/*"]
}
}
Web page:
// On https://mywebsite.com
chrome.runtime.sendMessage(
'extension-id-here',
{ type: 'FROM_WEBSITE', data: 'hello' },
(response) => {
console.log('Extension responded:', response);
}
);
Background listener:
chrome.runtime.onMessageExternal.addListener(
(message, sender, sendResponse) => {
// Verify sender
if (!sender.url.startsWith('https://mywebsite.com')) {
sendResponse({ error: 'Unauthorized' });
return;
}
handleExternalMessage(message)
.then(sendResponse);
return true;
}
);
// Send to another extension
chrome.runtime.sendMessage(
'other-extension-id',
{ type: 'CROSS_EXT_MESSAGE' },
(response) => {}
);
// Receive from other extensions
chrome.runtime.onMessageExternal.addListener(
(message, sender, sendResponse) => {
console.log('From extension:', sender.id);
}
);
sender.id and sender.urlThis 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.