Examples
Real-world examples demonstrating common Chrome extension patterns and features.
Table of Contents
- Basic Popup with Settings
- Content Script UI Injection
- Background Task Scheduler
- Cross-Context Communication
- Persistent Storage with Sync
- Dynamic Content Script Injection
- Context Menu Integration
- Tab Management
Basic Popup with Settings
Create a popup that saves user preferences.
Popup Component
src/pages/popup/index.tsx
import { render } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { kv } from '@/shared/lib/storage';
import { t } from '@/shared/lib/i18n';
import '@/shared/styles.css';
const Popup = () => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const [loading, setLoading] = useState(true);
useEffect(() => {
// Load settings on mount
const loadSettings = async () => {
const settings = await kv.get('sync', 'settings', { theme: 'light' });
setTheme(settings.theme);
setLoading(false);
};
loadSettings();
}, []);
const handleThemeChange = async (newTheme: 'light' | 'dark') => {
setTheme(newTheme);
const settings = await kv.get('sync', 'settings', {});
await kv.set('sync', 'settings', { ...settings, theme: newTheme });
};
if (loading) {
return <div className="p-4">Loading...</div>;
}
return (
<div className="w-64 p-4">
<h1 className="text-lg font-bold mb-4">{t('extName')}</h1>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Theme</label>
<div className="flex gap-2">
<button
onClick={() => handleThemeChange('light')}
className={`px-4 py-2 rounded ${
theme === 'light' ? 'bg-blue-500 text-white' : 'bg-gray-200'
}`}>
Light
</button>
<button
onClick={() => handleThemeChange('dark')}
className={`px-4 py-2 rounded ${
theme === 'dark' ? 'bg-blue-500 text-white' : 'bg-gray-200'
}`}>
Dark
</button>
</div>
</div>
<button
onClick={() => chrome.runtime.openOptionsPage()}
className="w-full px-4 py-2 bg-gray-100 rounded hover:bg-gray-200">
More Settings
</button>
</div>
);
};
render(<Popup />, document.getElementById('root')!);
Content Script UI Injection
Inject a floating widget into web pages.
src/content/widget.tsx
import { render } from 'preact';
import { useState } from 'preact/hooks';
import { mount } from '@/shared/lib/dom';
import { bus } from '@/shared/lib/messaging';
import { MSG } from '@/shared/constants';
const FloatingWidget = () => {
const [visible, setVisible] = useState(true);
const [position, setPosition] = useState({ x: 20, y: 20 });
const [isDragging, setIsDragging] = useState(false);
const handleMouseDown = (e: MouseEvent) => {
setIsDragging(true);
const startX = e.clientX - position.x;
const startY = e.clientY - position.y;
const handleMouseMove = (e: MouseEvent) => {
setPosition({
x: e.clientX - startX,
y: e.clientY - startY
});
};
const handleMouseUp = () => {
setIsDragging(false);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
if (!visible) return null;
return (
<div
style={{
position: 'fixed',
top: `${position.y}px`,
left: `${position.x}px`,
zIndex: 9999,
cursor: isDragging ? 'grabbing' : 'grab'
}}
onMouseDown={handleMouseDown}
className="bg-white shadow-lg rounded-lg p-4 border border-gray-300">
<div className="flex justify-between items-center mb-2">
<h3 className="font-bold">Extension Widget</h3>
<button
onClick={() => setVisible(false)}
className="text-gray-500 hover:text-gray-700">
✕
</button>
</div>
<p className="text-sm text-gray-600">Drag me around!</p>
</div>
);
};
// Mount widget
const container = mount('ces-widget');
render(<FloatingWidget />, container);
Background Task Scheduler
Schedule periodic tasks using Chrome alarms.
src/background/alarms.ts
import { logger } from '@/shared/lib/logger';
import { kv } from '@/shared/lib/storage';
import { ALARMS } from '@/shared/constants';
// Create alarms on installation
chrome.runtime.onInstalled.addListener(() => {
// Poll every 5 minutes
chrome.alarms.create(ALARMS.POLL, { periodInMinutes: 5 });
// Daily cleanup at midnight
chrome.alarms.create(ALARMS.DAILY_CLEANUP, {
when: getNextMidnight(),
periodInMinutes: 24 * 60
});
logger.info('Alarms created');
});
// Handle alarm events
chrome.alarms.onAlarm.addListener(async (alarm) => {
logger.info('Alarm triggered', { name: alarm.name });
switch (alarm.name) {
case ALARMS.POLL:
await pollData();
break;
case ALARMS.DAILY_CLEANUP:
await dailyCleanup();
break;
}
});
// Poll external API
const pollData = async () => {
try {
const response = await fetch('https://api.example.com/updates');
const data = await response.json();
// Store latest data
await kv.set('local', 'latestData', data);
logger.info('Data polled successfully', data);
// Notify user if needed
if (data.hasUpdate) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon128.png',
title: 'New Update Available',
message: data.message
});
}
} catch (err) {
logger.error('Failed to poll data', err);
}
};
// Clean up old data
const dailyCleanup = async () => {
try {
const allLocal = await kv.getAll('local');
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000; // 7 days
const cleaned = Object.entries(allLocal).reduce((acc, [key, value]) => {
if (key.startsWith('cache_') && value.timestamp < cutoff) {
// Skip old cache entries
return acc;
}
acc[key] = value;
return acc;
}, {} as Record<string, any>);
await kv.setAll('local', cleaned);
logger.info('Daily cleanup completed');
} catch (err) {
logger.error('Failed to clean up data', err);
}
};
// Helper to get next midnight timestamp
const getNextMidnight = (): number => {
const now = new Date();
const midnight = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate() + 1,
0, 0, 0
);
return midnight.getTime();
};
Cross-Context Communication
Communicate between popup, content, and background scripts.
src/shared/constants.ts
export enum MSG {
CHANGE_BG = 'CHANGE_BG',
GET_PAGE_INFO = 'GET_PAGE_INFO',
NOTIFY_USER = 'NOTIFY_USER'
}
export const MESSAGE_SPEC = {
[MSG.CHANGE_BG]: {
req: {} as { color: string },
res: {} as { ok: boolean }
},
[MSG.GET_PAGE_INFO]: {
req: {} as {},
res: {} as { title: string; url: string }
},
[MSG.NOTIFY_USER]: {
req: {} as { message: string },
res: {} as { shown: boolean }
}
} as const;
src/pages/popup/index.tsx
import { bus } from '@/shared/lib/messaging';
import { MSG } from '@/shared/constants';
const Popup = () => {
const handleGetPageInfo = async () => {
const info = await bus.sendToActive(MSG.GET_PAGE_INFO, {});
if (info) {
alert(`Page: ${info.title}\nURL: ${info.url}`);
}
};
const handleChangeBackground = async () => {
const result = await bus.sendToActive(MSG.CHANGE_BG, { color: '#0ea5e9' });
if (result?.ok) {
alert('Background changed!');
}
};
return (
<div className="p-4 space-y-2">
<button onClick={handleGetPageInfo} className="btn-primary">
Get Page Info
</button>
<button onClick={handleChangeBackground} className="btn-primary">
Change Background
</button>
</div>
);
};
src/content/index.tsx
import { bus } from '@/shared/lib/messaging';
import { MSG } from '@/shared/constants';
// Handle page info request
bus.on(MSG.GET_PAGE_INFO, () => {
return {
title: document.title,
url: window.location.href
};
});
// Handle background color change
bus.on(MSG.CHANGE_BG, (payload) => {
document.body.style.backgroundColor = payload.color;
return { ok: true };
});
src/background/runtime.ts
import { bus } from '@/shared/lib/messaging';
import { MSG } from '@/shared/constants';
// Handle notification request
bus.on(MSG.NOTIFY_USER, async (payload) => {
await chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon128.png',
title: 'Extension Notification',
message: payload.message
});
return { shown: true };
});
Persistent Storage with Sync
Sync user preferences across Chrome instances.
src/shared/lib/settings.ts
import { kv } from '@/shared/lib/storage';
export interface AppSettings {
theme: 'light' | 'dark' | 'auto';
language: 'en' | 'ja' | 'zh_TW';
notifications: {
enabled: boolean;
sound: boolean;
};
}
const DEFAULT_SETTINGS: AppSettings = {
theme: 'auto',
language: 'en',
notifications: {
enabled: true,
sound: false
}
};
export const getSettings = async (): Promise<AppSettings> => {
const stored = await kv.get('sync', 'settings');
return { ...DEFAULT_SETTINGS, ...stored };
};
export const updateSettings = async (updates: Partial<AppSettings>): Promise<void> => {
const current = await getSettings();
const newSettings = { ...current, ...updates };
await kv.set('sync', 'settings', newSettings);
};
// Listen for changes from other instances
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'sync' && changes.settings) {
console.log('Settings synced from another device:', changes.settings.newValue);
// Update UI or apply new settings
}
});
Usage:
const OptionsPage = () => {
const [settings, setSettings] = useState<AppSettings>(DEFAULT_SETTINGS);
useEffect(() => {
getSettings().then(setSettings);
}, []);
const handleSave = async () => {
await updateSettings(settings);
alert('Settings saved and synced!');
};
return (
<div>
<select
value={settings.theme}
onChange={(e) => setSettings({ ...settings, theme: e.target.value })}>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="auto">Auto</option>
</select>
<button onClick={handleSave}>Save</button>
</div>
);
};
Dynamic Content Script Injection
Inject content scripts programmatically into specific tabs.
src/background/injection.ts
import { logger } from '@/shared/lib/logger';
// Inject content script into tab
export const injectContentScript = async (tabId: number): Promise<boolean> => {
try {
await chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
});
await chrome.scripting.insertCSS({
target: { tabId },
files: ['content.css']
});
logger.info('Content script injected', { tabId });
return true;
} catch (err) {
logger.error('Failed to inject content script', err);
return false;
}
};
// Inject into all eligible tabs
export const injectAllTabs = async (): Promise<void> => {
const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
if (tab.id && tab.url && !isRestrictedUrl(tab.url)) {
await injectContentScript(tab.id);
}
}
};
// Check if URL is restricted
const isRestrictedUrl = (url: string): boolean => {
try {
const urlObj = new URL(url);
const restrictedSchemes = ['chrome', 'chrome-extension', 'edge', 'about'];
return restrictedSchemes.includes(urlObj.protocol.replace(':', ''));
} catch {
return true;
}
};
// Auto-inject on tab update
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url && !isRestrictedUrl(tab.url)) {
await injectContentScript(tabId);
}
});
Context Menu Integration
Add custom context menu items.
src/background/contextMenu.ts
import { logger } from '@/shared/lib/logger';
import { bus } from '@/shared/lib/messaging';
import { MSG } from '@/shared/constants';
// Create context menu on installation
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'highlight-text',
title: 'Highlight "%s"',
contexts: ['selection']
});
chrome.contextMenus.create({
id: 'change-bg-red',
title: 'Change Background to Red',
contexts: ['page']
});
chrome.contextMenus.create({
id: 'change-bg-blue',
title: 'Change Background to Blue',
contexts: ['page']
});
logger.info('Context menus created');
});
// Handle context menu clicks
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
if (!tab?.id) return;
switch (info.menuItemId) {
case 'highlight-text':
await bus.sendToTab(tab.id, MSG.HIGHLIGHT_TEXT, {
text: info.selectionText || ''
});
break;
case 'change-bg-red':
await bus.sendToTab(tab.id, MSG.CHANGE_BG, { color: '#ef4444' });
break;
case 'change-bg-blue':
await bus.sendToTab(tab.id, MSG.CHANGE_BG, { color: '#3b82f6' });
break;
}
});
Tab Management
Manage tabs programmatically.
src/background/tabs.ts
import { logger } from '@/shared/lib/logger';
// Get active tab
export const getActiveTab = async (): Promise<chrome.tabs.Tab | undefined> => {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
return tabs[0];
};
// Open new tab
export const openTab = async (url: string): Promise<chrome.tabs.Tab> => {
return chrome.tabs.create({ url });
};
// Close tabs matching pattern
export const closeTabsByUrl = async (pattern: string): Promise<void> => {
const tabs = await chrome.tabs.query({});
const toClose = tabs.filter(tab => tab.url?.includes(pattern));
for (const tab of toClose) {
if (tab.id) {
await chrome.tabs.remove(tab.id);
}
}
logger.info('Closed tabs', { count: toClose.length, pattern });
};
// Duplicate tab
export const duplicateTab = async (tabId: number): Promise<chrome.tabs.Tab> => {
return chrome.tabs.duplicate(tabId);
};
// Move tab to new window
export const moveTabToNewWindow = async (tabId: number): Promise<void> => {
await chrome.windows.create({ tabId });
};
Next Steps
- Explore Core Modules for more utilities
- Check API Reference for type definitions
- Read Development Guide for workflow tips