Implementing Notifications from a Browser Extension
Browser extensions can display system notifications through the chrome.notifications API. These are native OS notifications, not page popups — they appear in the system tray even when the browser is minimized.
Permission in Manifest
{
"permissions": ["notifications"]
}
Notification Types
basic — title + text + icon. The most common type.
image — with a large image. Not displayed on macOS (ignored).
list — a list of items. Support depends on the OS.
progress — progress bar. Useful for background downloads.
Creating a Notification
// background/sw.js
async function showNotification(id, options) {
return new Promise((resolve) => {
chrome.notifications.create(id, {
type: 'basic',
iconUrl: chrome.runtime.getURL('icons/icon-128.png'),
title: options.title,
message: options.message,
priority: 1, // 0 = low, 1 = normal, 2 = high
requireInteraction: options.persistent ?? false, // don't hide automatically
buttons: options.buttons ?? [],
silent: options.silent ?? false
}, resolve);
});
}
// Usage
await showNotification('sync-complete', {
title: 'Synchronization Complete',
message: 'Added 3 new entries',
buttons: [{ title: 'Open' }]
});
If you pass an empty string as id, the browser will generate a unique id and return it via callback.
Notification with Progress
async function showProgress(jobId, title, progress) {
const exists = await notificationExists(jobId);
if (!exists) {
chrome.notifications.create(jobId, {
type: 'progress',
iconUrl: chrome.runtime.getURL('icons/icon-128.png'),
title,
message: `${progress}%`,
progress
});
} else {
chrome.notifications.update(jobId, {
progress,
message: `${progress}%`
});
}
}
function notificationExists(id) {
return new Promise((resolve) => {
chrome.notifications.getAll((all) => resolve(id in all));
});
}
// Example usage for file downloads
async function downloadWithProgress(url, filename) {
const jobId = `download-${Date.now()}`;
await showProgress(jobId, `Downloading: ${filename}`, 0);
const response = await fetch(url);
const total = parseInt(response.headers.get('content-length') ?? '0');
const reader = response.body.getReader();
let received = 0;
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
if (total > 0) {
await showProgress(jobId, `Downloading: ${filename}`, Math.round(received / total * 100));
}
}
chrome.notifications.clear(jobId);
return new Blob(chunks);
}
Handling Notification Clicks
chrome.notifications.onClicked.addListener(async (notificationId) => {
chrome.notifications.clear(notificationId);
// Open the required tab or focus on existing one
if (notificationId.startsWith('new-message-')) {
const messageId = notificationId.split('-').at(-1);
await openOrFocusTab(`/messages/${messageId}`);
}
});
chrome.notifications.onButtonClicked.addListener(async (notificationId, buttonIndex) => {
chrome.notifications.clear(notificationId);
if (notificationId === 'sync-complete' && buttonIndex === 0) {
// "Open" button
await chrome.tabs.create({ url: chrome.runtime.getURL('pages/dashboard.html') });
}
});
async function openOrFocusTab(path) {
const url = chrome.runtime.getURL(`pages/app.html${path}`);
const [existing] = await chrome.tabs.query({ url: `${chrome.runtime.getURL('pages/app.html')}*` });
if (existing) {
await chrome.tabs.update(existing.id, { active: true, url });
await chrome.windows.update(existing.windowId, { focused: true });
} else {
await chrome.tabs.create({ url });
}
}
Notifications for Periodic Tasks
A typical pattern — notifications from alarm:
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name !== 'check-updates') return;
const updates = await fetchUpdates();
if (updates.length === 0) return;
if (updates.length === 1) {
chrome.notifications.create('update-1', {
type: 'basic',
iconUrl: chrome.runtime.getURL('icons/icon-128.png'),
title: updates[0].title,
message: updates[0].body,
contextMessage: new URL(updates[0].url).hostname
});
} else {
chrome.notifications.create('updates-batch', {
type: 'list',
iconUrl: chrome.runtime.getURL('icons/icon-128.png'),
title: `${updates.length} new updates`,
message: '',
items: updates.slice(0, 8).map(u => ({
title: u.title,
message: new URL(u.url).hostname
}))
});
}
});
On Windows, notifications go to the Action Center. On macOS, to the Notification Center. On Linux, through libnotify, appearance depends on the DE. Keep this in mind when testing: on macOS, the list and image types don't display additional content.







