# Notification actions and badge options

> The actions and badge options of showNotification(): their fields, the two-button limit behind Notification.maxActions, the 96 by 96 badge, and engine support.

`actions` adds buttons to a persistent notification and `badge` supplies the small monochrome
image the OS shows where the full notification does not fit, such as the Android status bar.
Both are members of the `options` dictionary of `ServiceWorkerRegistration.showNotification()`,
both are Chromium-only in shipping browsers apart from Firefox 152's `actions`, and neither has
anything to do with the app-icon count set by the Badging API.

## Syntax

```js
registration.showNotification(title, {
  badge: '/icons/badge-96.png',
  actions: [{ action: 'reply', title: 'Reply', icon: '/icons/reply.png' }],
})
Notification.maxActions
```

`actions` is accepted only by `showNotification()`; passing a non-empty array to
`new Notification()` throws `TypeError`. `Notification.maxActions` is a static getter returning
how many buttons the engine displays. Support: `actions` in Chrome 53, Edge 18, Firefox 152;
`badge` in Chrome 53 and Edge 18; neither in Safari (BCD `api.Notification.actions`,
`api.Notification.badge`).

## Members

| Member | Type | Meaning |
|---|---|---|
| `actions[].action` | string | Identifier returned as `event.action` in `notificationclick`. Required. |
| `actions[].title` | string | Button label. Required. |
| `actions[].icon` | string (URL), optional | Image shown on the button where the platform draws icons (Android; not Windows). |
| `actions[].navigate` | string (URL), optional | Safari 18.4: open this URL instead of firing `notificationclick`. Chromium ignores it. |
| `badge` | string (URL) | Image for the compact representation. Android masks it to a silhouette and shows it at up to 4x density, so a 96 by 96 pixel single-colour PNG with transparency is the working size. |

Chromium displays at most `kMaximumActions = 2` buttons ([notification.mojom](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/public/mojom/notifications/notification.mojom),
chromium.googlesource.com) and drops the rest without error; Firefox 152 reports its own limit
through `maxActions`. Read the getter rather than hardcoding two.

## Exceptions

| Exception | When |
|---|---|
| `TypeError` | `actions` is non-empty on `new Notification()`. Chromium: `Actions are only supported for persistent notifications shown using ServiceWorkerRegistration.showNotification().` |
| `TypeError` | An action object lacks `action` or `title`, which are required dictionary members. |

An unsupported option is not an exception: Firefox before 152 and Safari ignore `actions`, and
every engine except Chromium ignores `badge`, so the notification displays with the body only.

## Support position

`actions` and `badge` shipped together in Chrome 53 (2016) and reached Edge with its move to
Chromium; Firefox added `actions` in 152 and still ignores `badge`; Safari supports neither on
macOS or iOS, where notification buttons are not part of the web notification surface (BCD
`api.Notification.actions`, `api.Notification.badge`). On Android the badge is visible in the
status bar and lock screen; on Windows and macOS Chromium ignores `badge` because the OS has no
compact slot for it.

## Examples

The examples pair a page-side `showNotification()` call with the service worker handler that reads `event.action`.

### Two buttons with a handler per action

The service worker reads `event.action`. The empty string means the body was tapped; anything
else is one of the `action` ids.

```js
// page
await registration.showNotification('New message from Ada', {
  body: 'Can we move the call to 3pm?',
  badge: '/icons/badge-96.png',
  tag: 'thread-42',
  data: { threadId: 42 },
  actions: [
    { action: 'reply', title: 'Reply' },
    { action: 'mute', title: 'Mute thread' },
  ],
});

// service worker
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  const { threadId } = event.notification.data;
  if (event.action === 'mute') {
    event.waitUntil(fetch(`/api/threads/${threadId}/mute`, { method: 'POST' }));
  } else {
    event.waitUntil(self.clients.openWindow(`/threads/${threadId}${event.action === 'reply' ? '#compose' : ''}`));
  }
});
```

Because the limit is two in Chromium, put the primary and the destructive action in that order;
a third button would be dropped.

### Sizing the badge image

The badge is drawn as a silhouette: every opaque pixel becomes white (or the system accent),
so a logo with internal detail turns into a blob. Export a one-colour glyph with a transparent
background at 96 by 96 pixels.

```js
await registration.showNotification('Download complete', {
  body: 'report-2026-q3.pdf',
  icon: '/icons/icon-192.png',   // full-colour, shown beside the text
  badge: '/icons/badge-96.png',  // single colour, shown in the status bar
});
```

The `icon` and `badge` serve different slots and are both worth setting; a notification with only
`icon` shows the browser's own glyph in the Android status bar.

### Detecting the options and omitting them where unsupported

The options are properties of `Notification.prototype` where supported. Build the options object
conditionally so the same call works in Safari and Firefox.

```js
function notificationOptions(base, buttons) {
  if (!('Notification' in window)) return null; // no notifications at all
  const options = { ...base };
  if ('badge' in Notification.prototype) options.badge = '/icons/badge-96.png';
  if ('actions' in Notification.prototype && Notification.maxActions > 0) {
    options.actions = buttons.slice(0, Notification.maxActions);
  }
  return options;
}
```

Slicing to `maxActions` keeps the first buttons rather than letting the engine drop the last ones
arbitrarily, which matters when the array order is "primary, secondary, destructive".

:::observed
`Notification.maxActions` returns `2` in Chrome and Edge on Windows, macOS, and Android, the
constant `kMaximumActions = 2` in Chromium's
[notification.mojom](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/public/mojom/notifications/notification.mojom)
(chromium.googlesource.com), and `undefined` in Safari 18, where neither the static nor
`'actions' in Notification.prototype` exists. Passing three actions to `showNotification()` in Chrome displays the first two with no
console warning; passing one to `new Notification()` throws `TypeError: Failed to construct
'Notification': Actions are only supported for persistent notifications shown using
ServiceWorkerRegistration.showNotification().`
:::

## See also

- [Notifications API Standard: actions](https://notifications.spec.whatwg.org/#dom-notification-actions) (whatwg.org)
- [Notification: actions property](https://developer.mozilla.org/en-US/docs/Web/API/Notification/actions) (developer.mozilla.org)
- [Notifications API](/reference/notifications/notifications-api/)
- [Notification interface](/reference/notifications/notification/)
- [Badging API](/reference/installation/badging/)