Browser vendors continuously add new APIs that expand what extensions can do. Staying current with these APIs lets you build more capable extensions with better user experiences. This guide covers the most impactful new browser APIs and how to use them effectively.
Side Panel API
The Side Panel API (Chrome 114+) lets extensions display content in a persistent panel beside web pages—perfect for AI assistants, note-taking, and productivity tools.
// manifest.json
{
"manifest_version": 3,
"side_panel": {
"default_path": "sidepanel.html"
},
"permissions": ["sidePanel"]
}
// background.js
// Open side panel when extension icon is clicked
chrome.action.onClicked.addListener(async (tab) => {
await chrome.sidePanel.open({ windowId: tab.windowId });
});
// Set panel behavior per tab
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (changeInfo.url) {
// Show different panels for different sites
if (tab.url.includes('github.com')) {
await chrome.sidePanel.setOptions({
tabId,
path: 'panels/github-assistant.html',
enabled: true
});
}
}
});
<!-- sidepanel.html -->
<!DOCTYPE html>
<html>
<head>
<style>
body {
width: 100%;
height: 100vh;
margin: 0;
padding: 16px;
box-sizing: border-box;
font-family: system-ui;
}
.chat-container {
display: flex;
flex-direction: column;
height: 100%;
}
.messages {
flex: 1;
overflow-y: auto;
}
.input-area {
padding-top: 16px;
}
</style>
</head>
<body>
<div class="chat-container">
<div class="messages" id="messages"></div>
<div class="input-area">
<textarea id="input" placeholder="Ask anything..."></textarea>
<button id="send">Send</button>
</div>
</div>
<script src="sidepanel.js"></script>
</body>
</html>
- Keep the panel width-responsive (users can resize)
- Implement keyboard shortcuts for quick access
- Remember scroll position when panel reopens
- Use postMessage to communicate with content scripts
Declarative Net Request API
Replace the deprecated webRequestBlocking with declarativeNetRequest for content blocking and request modification:
// manifest.json
{
"manifest_version": 3,
"permissions": ["declarativeNetRequest"],
"declarative_net_request": {
"rule_resources": [{
"id": "ruleset_1",
"enabled": true,
"path": "rules.json"
}]
}
}
// rules.json
[
{
"id": 1,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "||ads.example.com",
"resourceTypes": ["script", "image"]
}
},
{
"id": 2,
"priority": 2,
"action": {
"type": "redirect",
"redirect": { "extensionPath": "/blocked.html" }
},
"condition": {
"urlFilter": "||malware-site.com",
"resourceTypes": ["main_frame"]
}
},
{
"id": 3,
"priority": 1,
"action": {
"type": "modifyHeaders",
"responseHeaders": [
{ "header": "X-Frame-Options", "operation": "remove" }
]
},
"condition": {
"urlFilter": "*",
"resourceTypes": ["sub_frame"]
}
}
]
Dynamic Rules
Add rules at runtime for user-configured blocking:
// Add a user-defined blocking rule
async function blockDomain(domain) {
const rules = await chrome.declarativeNetRequest.getDynamicRules();
const newRuleId = Math.max(0, ...rules.map(r => r.id)) + 1;
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [{
id: newRuleId,
priority: 1,
action: { type: 'block' },
condition: {
urlFilter: `||${domain}`,
resourceTypes: ['main_frame', 'sub_frame', 'script', 'image', 'xmlhttprequest']
}
}]
});
return newRuleId;
}
// Remove a rule
async function unblockDomain(ruleId) {
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [ruleId]
});
}
User Scripts API
The User Scripts API (Chrome 120+) lets users inject custom JavaScript into web pages:
// manifest.json
{
"manifest_version": 3,
"permissions": ["userScripts"],
"optional_host_permissions": ["*://*/*"]
}
// Register a user script
async function registerUserScript(script) {
await chrome.userScripts.register([{
id: script.id,
matches: script.matches,
js: [{ code: script.code }],
runAt: 'document_idle',
world: 'MAIN' // Run in page context, not isolated
}]);
}
// Example: Add dark mode to any site
await registerUserScript({
id: 'dark-mode',
matches: ['*://*.example.com/*'],
code: `
document.documentElement.style.filter = 'invert(1) hue-rotate(180deg)';
document.querySelectorAll('img, video').forEach(el => {
el.style.filter = 'invert(1) hue-rotate(180deg)';
});
`
});
Reading List API
Access the browser’s built-in reading list (Chrome 91+):
// Add to reading list
async function saveForLater(url, title) {
try {
await chrome.readingList.addEntry({
url,
title,
hasBeenRead: false
});
return { success: true };
} catch (error) {
// Entry might already exist
return { success: false, error: error.message };
}
}
// Get all reading list items
async function getReadingList() {
const entries = await chrome.readingList.query({});
return entries;
}
// Mark as read
async function markAsRead(url) {
await chrome.readingList.updateEntry({
url,
hasBeenRead: true
});
}
// Remove from reading list
async function removeFromReadingList(url) {
await chrome.readingList.removeEntry({ url });
}
Offscreen Documents API
Create off-screen documents for tasks that need DOM APIs but shouldn’t be visible:
// manifest.json
{
"manifest_version": 3,
"permissions": ["offscreen"]
}
// background.js
async function createOffscreenDocument() {
const existingContexts = await chrome.runtime.getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT']
});
if (existingContexts.length > 0) return;
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_PARSER'],
justification: 'Parse HTML content from API responses'
});
}
// offscreen.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'PARSE_HTML') {
const parser = new DOMParser();
const doc = parser.parseFromString(message.html, 'text/html');
// Extract data using DOM APIs
const result = {
title: doc.querySelector('title')?.textContent,
headings: Array.from(doc.querySelectorAll('h1, h2')).map(h => h.textContent),
links: Array.from(doc.querySelectorAll('a[href]')).map(a => ({
text: a.textContent,
href: a.getAttribute('href')
}))
};
sendResponse(result);
}
return true;
});
- Audio playback (Web Audio API)
- HTML/DOM parsing
- Clipboard access
- Canvas rendering
- WebRTC connections
Storage API Improvements
Session Storage
Temporary storage that clears when the browser closes:
// Store session-only data
await chrome.storage.session.set({
tempAuthToken: 'abc123',
recentSearches: ['query1', 'query2']
});
// Access from any extension context
const { tempAuthToken } = await chrome.storage.session.get('tempAuthToken');
Storage Access for Service Workers
Service workers can now access storage synchronously during specific events:
// background.js (service worker)
chrome.storage.session.setAccessLevel({
accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS'
});
// Now content scripts can access session storage too
Scripting API Enhancements
Execute Scripts with Arguments
// Pass arguments to injected functions
async function highlightText(tabId, searchText, color) {
await chrome.scripting.executeScript({
target: { tabId },
func: (text, highlightColor) => {
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT
);
while (walker.nextNode()) {
const node = walker.currentNode;
if (node.textContent.includes(text)) {
const span = document.createElement('span');
span.style.backgroundColor = highlightColor;
const range = document.createRange();
range.selectNodeContents(node);
range.surroundContents(span);
}
}
},
args: [searchText, color]
});
}
Register Content Scripts Dynamically
// Add content scripts at runtime
async function enableFeatureForSite(pattern) {
await chrome.scripting.registerContentScripts([{
id: `feature-${pattern}`,
matches: [pattern],
js: ['feature.js'],
css: ['feature.css'],
runAt: 'document_idle'
}]);
}
// Update existing registered scripts
await chrome.scripting.updateContentScripts([{
id: 'feature-*://example.com/*',
css: ['feature-v2.css'] // Update CSS only
}]);
// Remove registered scripts
await chrome.scripting.unregisterContentScripts({
ids: ['feature-*://example.com/*']
});
Tabs API Improvements
Tab Groups
Organize tabs into collapsible groups:
// Create a tab group
async function groupRelatedTabs(tabIds, title, color) {
const groupId = await chrome.tabs.group({ tabIds });
await chrome.tabGroups.update(groupId, {
title,
color, // 'grey', 'blue', 'red', 'yellow', 'green', 'pink', 'purple', 'cyan'
collapsed: false
});
return groupId;
}
// Move tab to existing group
async function addToGroup(tabId, groupId) {
await chrome.tabs.group({
tabIds: [tabId],
groupId
});
}
// Query grouped tabs
const groups = await chrome.tabGroups.query({});
for (const group of groups) {
const tabs = await chrome.tabs.query({ groupId: group.id });
console.log(`Group "${group.title}": ${tabs.length} tabs`);
}
Action API
The unified Action API replaces browser_action and page_action:
// Dynamic badge
chrome.action.setBadgeText({ text: '5' });
chrome.action.setBadgeBackgroundColor({ color: '#FF0000' });
// Per-tab state
chrome.action.setIcon({
tabId: tab.id,
path: {
16: 'icons/active-16.png',
32: 'icons/active-32.png'
}
});
// Disable for specific tabs
chrome.action.disable(tab.id);
// Open popup programmatically (requires user gesture)
chrome.action.openPopup();
Feature Detection
Always check for API availability before using new features:
// Check if API exists
function hasFeature(apiPath) {
const parts = apiPath.split('.');
let obj = chrome;
for (const part of parts) {
if (obj[part] === undefined) return false;
obj = obj[part];
}
return true;
}
// Usage
if (hasFeature('sidePanel')) {
await chrome.sidePanel.open({ windowId });
} else {
// Fallback to popup or new tab
await chrome.windows.create({
url: 'sidepanel.html',
type: 'popup',
width: 400,
height: 600
});
}
Summary
Staying current with browser APIs lets you build more powerful, user-friendly extensions. Key APIs to consider:
- Side Panel: Persistent UI alongside web content
- Declarative Net Request: Efficient content blocking
- User Scripts: Custom code injection
- Offscreen Documents: Background DOM operations
- Tab Groups: Organize user tabs
Always implement feature detection and graceful fallbacks for broader browser compatibility.