Web Notifications vs chrome.notifications

Choose between the Web Notifications API (registration.showNotification) and chrome.notifications in an MV3 extension: permissions, features, service worker support, click handling, cross-browser reach, and a wrapper for both.

Published October 2, 2026 Updated October 2, 2026 7 min read
Table of Contents

An extension’s service worker is a real service worker, so it can call self.registration.showNotification() — the same Web Notifications API websites use — as well as chrome.notifications.create(). Both put a notification on screen, both handle clicks, and both are delivered by the same system notification center. They differ in permissions, features, how events reach your code, and which browsers support them. Picking one without knowing the trade-offs leads to features that work in Chrome and silently fail in Firefox, or to a permission prompt users never expected. This guide compares the two and shows a wrapper that uses the right one. It belongs to notifications, badges and the action API.

Two APIs, one notification center

chrome.notifications is the extension API: declared with the notifications permission, it offers basic, image, list and progress templates, up to two buttons, explicit update/clear/getAll, and events (onClicked, onButtonClicked, onClosed). The Web Notifications API, available in the extension service worker through self.registration.showNotification(title, options), offers body, icon, badge, image, tag (for replacement), requireInteraction, silent, data, and actions (buttons), with clicks delivered as notificationclick events on the service worker. In extensions, the Web API’s permission is granted by declaring notifications in the manifest — no runtime prompt. Firefox supports a subset of chrome.notifications and the Web API; Safari supports neither for extensions in the same way.

chrome.notifications versus Web Notifications in an extensionThe two notification APIs compared on templates, buttons, update and clear, structured data, click delivery and cross-browser support.Featurechrome.notificationsregistration.showNotificationTemplatesbasic, image, list, progressBody, image, iconButtonsUp to 2actions (platform max)Replace / updateID + update()tag + renotifyList showngetAll()getNotifications()Attached dataVia ID + storageoptions.dataClick eventnotifications.onClickednotificationclick
chrome.notifications has templates and getAll; the Web API has data payloads and broader web parity.

Step-by-step: choosing and wrapping

1. Show a notification with the extension API

1await chrome.notifications.create("reminder-17", {
2  type: "basic",
3  iconUrl: "icons/128.png",
4  title: "Read later",
5  message: "“How to bake bread” — saved 3 days ago",
6  buttons: [{ title: "Open" }, { title: "Snooze" }],
7});
8chrome.notifications.onButtonClicked.addListener((id, i) => { /* route by id */ });

Execution context: the service worker. The ID is your only handle; attach context through the ID and storage. See handling notification clicks and buttons.

2. Show the same notification with the Web API

 1await self.registration.showNotification("Read later", {
 2  body: "“How to bake bread” — saved 3 days ago",
 3  icon: "icons/128.png",
 4  tag: "reminder-17",
 5  data: { itemId: 17, url: "https://example.com/bread" },
 6  actions: [{ action: "open", title: "Open" }, { action: "snooze", title: "Snooze" }],
 7});
 8
 9self.addEventListener("notificationclick", (event) => {
10  event.notification.close();
11  const { itemId, url } = event.notification.data;
12  if (event.action === "snooze") return event.waitUntil(snooze(itemId));
13  event.waitUntil(clients.openWindow(url));
14});

Execution context: the service worker. data carries structured context with the notification itself, so the click handler needs no storage lookup. tag makes a later notification with the same tag replace this one. event.waitUntil keeps the worker alive until the handler’s work finishes. clients.openWindow opens a tab; in extensions, chrome.tabs.create also works.

How clicks reach your codeWith chrome.notifications, a click fires notifications.onClicked with the ID, and the handler looks up context in storage; with the Web API, a click fires notificationclick with the Notification object including its data payload and the action chosen.chrome.notifications clickonClicked(id)Parse ID"reminder-17"storage.getitem contextversusWeb notification clicknotificationclickevent.notification.data{itemId, url}event.action"open" / "snooze"
Both wake the worker; the Web API brings its context with it.

3. Pick per feature

1choose.md
2Need progress bar or list template?        → chrome.notifications
3Need getAll() across worker restarts?      → either (getAll / getNotifications)
4Need structured data on the notification?  → Web API (data)
5Need Firefox buttons?                      → neither reliably; design without them
6Sharing code with a PWA/website?           → Web API

Execution context: a design note. Most extensions can use either. chrome.notifications is the conventional choice and has the progress template; the Web API is attractive when you share notification code with a web app, or want the data payload.

4. Wrap both behind one interface

 1// notify.js
 2const hasExt = typeof chrome !== "undefined" && chrome.notifications?.create;
 3
 4export async function notify({ id, title, body, icon = "icons/128.png", buttons = [], data = {} }) {
 5  if (hasExt) {
 6    await chrome.storage.session.set({ [`n:${id}`]: data });
 7    return chrome.notifications.create(id, {
 8      type: "basic", iconUrl: icon, title, message: body,
 9      ...(buttons.length && !isFirefox() ? { buttons: buttons.map((b) => ({ title: b.title })) } : {}),
10    });
11  }
12  return self.registration.showNotification(title, {
13    body, icon, tag: id, data: { id, ...data },
14    actions: buttons.map((b) => ({ action: b.action, title: b.title })),
15  });
16}

Execution context: the service worker. One function, one shape of input, two backends. Button support is dropped where it won’t render. Context goes into storage.session for the extension API and into data for the Web API, so the click router receives the same data either way.

Which API for this notification?Decision tree: progress bars or list templates need chrome.notifications; code shared with a website suggests the Web API; otherwise chrome.notifications is the default, with the Web API as a fallback where it is unavailable.What does the notification need?progress / list templatechrome.notificationstype: progress, listshared with web appWeb NotificationsshowNotificationplain alertchrome.notificationsWeb API as fallback
Default to chrome.notifications; reach for the Web API for shared code or data payloads.

5. Do not mix them for one notification kind

If reminders use the extension API and deals use the Web API, two click handlers must stay in sync and getAll sees only half the notifications. Pick one backend per extension (or per platform) through the wrapper, and route all clicks through one function.

6. Avoid notifications from extension pages

Extension pages (popup, options) can call new Notification(...) directly, but those notifications are tied to the page — clicks are lost when it closes. Always create notifications from the service worker, sending it a message from pages when needed.

7. Respect the user’s notification settings

Both APIs are subject to the operating system’s notification settings and the browser’s per-extension block. chrome.notifications.getPermissionLevel() reports denied when the user blocked them; in that case, surface the information in the badge or UI instead. See notification permissions and alert fatigue.

8. Route clicks from both backends to one handler

 1// sw.js — top level
 2async function onNotificationActivated(id, action) {
 3  const data = (await chrome.storage.session.get(`n:${id}`))[`n:${id}`] ?? {};
 4  await ROUTES[id.split("-")[0]]?.(data, action);
 5}
 6
 7chrome.notifications?.onClicked.addListener((id) => onNotificationActivated(id, "default"));
 8chrome.notifications?.onButtonClicked.addListener((id, i) => onNotificationActivated(id, ["open", "snooze"][i]));
 9self.addEventListener("notificationclick", (e) => {
10  e.notification.close();
11  e.waitUntil(onNotificationActivated(e.notification.tag, e.action || "default"));
12});

Execution context: the service worker’s top level. Whichever API the wrapper used, clicks end up in one function with the same arguments: the notification ID, its data, and a named action. Button indexes from the extension API are mapped to the same action names the Web API uses, so the routing table does not care which backend showed the notification. Register all of these listeners synchronously at the top level so a click that wakes the worker is never missed.

Common mistakes

  • Using new Notification() in the popup. Clicks are lost when the popup closes.
  • Two APIs for different kinds. Split click handling and listing.
  • Buttons in Firefox. chrome.notifications buttons are not supported there.
  • Forgetting event.waitUntil. The worker may stop before the click handler finishes.
  • Requesting permission at runtime. In extensions the manifest permission covers both.

Cross-browser variation

  • Chrome / Edge: both APIs available in the extension service worker with the notifications permission.
  • Firefox: browser.notifications supports basic (and partially image/list) without buttons or update; Web Notifications in the background are supported. Test both for your use.
  • Safari: extensions have no notifications API; the containing macOS app can post user notifications if the feature needs them.

Verification

  1. Show a notification through each backend and confirm clicks reach the same router with the same data.
  2. Stop the worker, click the notification and confirm it is handled.
  3. Block notifications for the extension and confirm the fallback (badge or UI) appears.
  4. Run the Firefox build and confirm no buttons are attempted.

FAQ

Does the Web API need Notification.requestPermission in extensions?

No. Declaring notifications in the manifest grants it.

Can Web notifications show progress bars?

No. Use chrome.notifications with type: "progress" or show progress in text.

Do the two APIs look different to users?

On most platforms both go through the same system notification center and look nearly identical.

Can I use push messages to trigger notifications?

Yes — an extension service worker can receive Web Push with pushManager.subscribe and show a notification from the push event. See receiving server push in an extension.

Which API should a new extension start with?

chrome.notifications, behind a small wrapper. It is the documented extension API, it supports progress, and the wrapper leaves room to switch later.

Other UI/UX Patterns & Interactive Components Resources