# The notification permission model

> How Notification.permission's three states work, why requestPermission() needs a user gesture, and why MDN says to stop prompting once a user has said no.

import CompatTable from '@components/CompatTable.astro';

**In one line:** `Notification.permission` reports one of three states —
`default`, `granted`, or `denied` — and per MDN, code should only call
`Notification.requestPermission()` in response to a real user gesture, never on load.

## The three states

Per MDN's `Notification.permission` reference:

- **`default`** — the user has not made a choice yet; the browser treats this the
  same as `denied` until the user grants permission.
- **`granted`** — the user accepted; on desktop the page may construct
  notifications with the `Notification` constructor (per MDN, most mobile
  browsers throw instead, and expect
  `ServiceWorkerRegistration.showNotification()`).
- **`denied`** — the user refused; notifications will not be displayed.

## Requesting permission from a user gesture

Per MDN's `Using the Notifications API` guide, permission should be requested
in response to a user gesture such as a click, and browsers are increasingly
disallowing requests that aren't — MDN names Firefox 72+ and Safari as
browsers that already enforce this. Ask in response to that gesture, not on load:

```js
document.getElementById('enable-notifications').addEventListener('click', async () => {
  if (!('Notification' in window)) {
    // Fallback: the Notification API doesn't exist in this browser at all.
    return;
  }
  const permission = await Notification.requestPermission();
  if (permission !== 'granted') {
    // Fallback: user declined (or dismissed) the prompt — don't proceed as if granted.
    return;
  }
  await showEnabledNotification();
});

async function showEnabledNotification() {
  // Per MDN, most mobile browsers expose `Notification` but throw a
  // TypeError from its constructor, expecting
  // `ServiceWorkerRegistration.showNotification()` instead — so route
  // through a registered service worker when one is available rather than
  // assuming the constructor works everywhere permission was granted.
  const registration = 'serviceWorker' in navigator
    ? await navigator.serviceWorker.getRegistration()
    : undefined;
  if (registration) {
    await registration.showNotification('Notifications enabled');
    return;
  }
  try {
    new Notification('Notifications enabled');
  } catch {
    // Fallback: no service worker registration and the constructor threw
    // (e.g. most mobile browsers) — nothing more to show without a
    // registered service worker.
  }
}
```

## After a denial, stop asking

MDN's own `requestPermission()` example code comments that once a user has
denied notifications, "there is no need to bother them any more" — don't call
`requestPermission()` again on a schedule hoping for a different answer.
MDN's own `Using the Notifications API` guide takes a lighter approach and
keeps its enable button visible after a denial, so the user has a chance to
change their mind later; either way, the request itself should not repeat
automatically.

## Push subscriptions are not exclusively behind a service worker

Per the Push API spec, a `PushManager` is available on both `Window` and
`ServiceWorkerRegistration`: a `Window`'s `PushManager` has a `null` associated
service worker registration, while a `ServiceWorkerRegistration`'s `PushManager`
is tied to that registration. The spec also defines window-accessible push
subscriptions and lets a declarative push message display a notification
without going through a page's own script. Requesting a push subscription via
`subscribe()` is its own permission flow, distinct from the `Notification`
permission described above.

## Where it is supported

Per MDN's browser-compat data, `Notification.permission` and
`requestPermission()` are **Limited availability** — not Baseline, because
they don't work the same way in some widely-used browsers. Desktop support is
broad (Chrome 32+, Edge 14+, Firefox 22+, Safari 7+), and Chrome, Firefox, and
Samsung Internet on Android support the permission property and
`requestPermission()` just as fully. WebView on Android has no support for
the permission API at all. Safari on iOS only exposes this to a web app the
user has added to the Home Screen (16.4+), not to ordinary browser tabs.
Requesting permission only from a user gesture is guidance MDN gives and some
browsers already enforce, rather than a spec requirement.

Note that the separate `Notification()` constructor has its own Android
restrictions on some browsers that are out of scope for this compatibility
table, since they don't apply to the permission property or
`requestPermission()` documented here.

<CompatTable feature="notification-permission" />

See [The Notifications API](/reference/notifications/notifications-api/)
and [Web Push](/reference/notifications/web-push/) for the per-browser
availability of the APIs this permission gates.

## Practical checklist

- [ ] Only call `requestPermission()` from inside a user gesture handler, never
      on page load.
- [ ] Treat `default` the same as "not permitted yet" — don't assume a request
      will succeed.
- [ ] Once `Notification.permission` is `denied`, don't call
      `requestPermission()` again on a schedule hoping for a different answer.
- [ ] Feature-detect `'Notification' in window` in page code before touching
      the API — per MDN, `Notification.permission` is also reachable from Web
      Workers, which have no `window`.
- [ ] Don't assume push subscription requires an active service worker
      registration — the Push API also defines a window-scoped path.

## Where to go next

- [The Notifications API](/reference/notifications/notifications-api/)
- [Web Push](/reference/notifications/web-push/)
- [The Notification interface](/reference/notifications/notification/)