# Badging API

> How setAppBadge() and clearAppBadge() put a count or dot on an installed app icon, which browsers draw it, the TypeError and no-op cases, and a fallback.

The Badging API (`navigator.setAppBadge()` and `navigator.clearAppBadge()`, also exposed on `WorkerNavigator` inside a service worker) places a number or a plain dot on an installed web app's icon in the taskbar, Dock, or Home Screen. Chrome 81 and Edge 81 draw it on Windows, macOS, and ChromeOS; Safari 16.4 draws it for Home Screen web apps on iOS and Safari 17 for Dock web apps on macOS; Chrome 84 and Samsung Internet 13 on Android honour it only for installed apps on launchers that show badges; Firefox has no implementation (BCD `api.Navigator.setAppBadge`).

## Syntax

```js
await navigator.setAppBadge();          // a dot (a "flag"), no number
await navigator.setAppBadge(contents);  // a number; 0 clears
await navigator.clearAppBadge();        // removes the badge

// Inside a service worker, for example from a push handler
self.navigator.setAppBadge(count);
```

Both methods return a `Promise<void>` that fulfils when the request has been handed to the operating system, not when the badge is visible. No permission prompt is involved on Chromium; on iOS the badge is only drawn once the user has allowed notifications for the installed app.

## Parameters

Only `setAppBadge()` takes an argument.

| Name | Type | Description |
|---|---|---|
| `contents` | `unsigned long long`, optional | The number to show. `0` clears the badge, the same as `clearAppBadge()`. When omitted, the badge is a dot without a number. The operating system decides how large numbers render; the specification calls the value a hint and allows the platform to abbreviate it. |

## Exceptions

`setAppBadge()` rejects or throws in two cases and silently does nothing in two others.

- `TypeError`: `contents` is negative, fractional, or otherwise outside the `[EnforceRange] unsigned long long` range. The conversion happens before any promise is created, so the error is synchronous in Chromium: `Failed to execute 'setAppBadge' on 'Navigator': Value is outside the 'unsigned long long' value range.`.
- `InvalidStateError`: the calling document is not fully active (for example, a detached iframe), per the specification's first step for both methods.
- No error, no badge: the app is not installed. Chromium resolves the promise in an ordinary tab without drawing anything, so a badge is only visible after installation.
- No error, no badge: on iOS 16.4 and later the Home Screen web app has not been granted notification permission (compat dataset note). The call resolves; the icon stays plain.

In Firefox `navigator.setAppBadge` is `undefined`, so an unguarded call throws `TypeError` before the API is reached.

:::observed
In Chrome's console (English UI), `navigator.setAppBadge(-1)` throws `TypeError: Failed to execute 'setAppBadge' on 'Navigator': Value is outside the 'unsigned long long' value range.`, while `await navigator.setAppBadge(3)` in an ordinary tab resolves to `undefined` and draws nothing; the same call from the app once installed from the demo at `/demo/#badging` shows "3" on the Dock or taskbar icon. The error text is Blink's standard message for `[EnforceRange]` conversions; the no-op in a tab matches the Chrome capability guide's statement that the badge applies to installed apps.
:::

## Examples

The first example runs in a service worker; the second runs in the page and covers browsers that lack the API.

### Mirroring an unread count from a push event

A push payload that carries the server's unread count can update the icon before the user opens the app. `event.waitUntil()` keeps the worker alive until the badge call settles; `clearAppBadge()` from the page when the inbox is viewed keeps the icon honest.

```js
// sw.js
self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? {};
  const unread = Number.isInteger(data.unread) ? data.unread : 0;
  event.waitUntil(Promise.all([
    self.registration.showNotification(data.title ?? 'New message'),
    self.navigator.setAppBadge(unread),
  ]));
});

// page.js, when the inbox view is shown
navigator.clearAppBadge?.();
```

Setting the badge without also showing a notification is allowed on Chromium, but on iOS a push event that shows no notification can cost the app its push subscription, so the notification call above is not optional there.

### Detecting support and falling back to the document title

Where `setAppBadge` is missing, the only cross-browser surface that can carry a count is the tab title. The fallback below prefixes it with the count and removes the prefix when the count is zero.

```js
function showUnread(count) {
  if ('setAppBadge' in navigator) {
    return count > 0 ? navigator.setAppBadge(count) : navigator.clearAppBadge();
  }
  // No Badging API (Firefox, or any browser in a plain tab): use the title.
  const base = document.title.replace(/^\(\d+\) /, '');
  document.title = count > 0 ? `(${count}) ${base}` : base;
  return Promise.resolve();
}
```

The title fallback is visible only while a tab is open, which is the limitation the Badging API removes; it is still the right behaviour for Firefox users rather than showing nothing.

## See also

- [Badging API specification](https://w3c.github.io/badging/) (w3c.github.io)
- [Badging for app icons](https://developer.chrome.com/docs/capabilities/web-apis/badging-api) (developer.chrome.com)
- [Badging API browser support](/compatibility/badging-api/)
- [Notification actions and badges](/reference/notifications/notification-actions-badge/)
- [Web Push](/reference/notifications/web-push/)
- [iOS Add to Home Screen](/reference/installation/ios-add-to-home-screen/)