# Clients API

> The Clients interface a service worker reaches as self.clients, its get(), matchAll(), openWindow(), and claim() members, the InvalidAccessError and InvalidStateError conditions, and the notification-click focus-or-open pattern.

`Clients`, exposed only inside a service worker as `self.clients`, is the worker's view of the documents and workers it controls or could control: it finds them, focuses them, opens new windows, and claims control of pages that loaded before the worker activated. The interface shipped with service workers themselves, in Chrome 40, Edge 17, Firefox 44, and Safari 11.1 (BCD `api.Clients`).

## Syntax

```js
// Inside the service worker only
const client = await self.clients.get(id);
const clients = await self.clients.matchAll(options);
const windowClient = await self.clients.openWindow(url);
await self.clients.claim();
```

All four methods return promises. `self.clients` is a `Clients` object; each result is a `Client` (for workers and documents) or a `WindowClient` (documents), which adds `focused`, `visibilityState`, `focus()`, and `navigate()`.

## Members

The interface has four methods and no attributes.

| Member | Returns | Behaviour |
|---|---|---|
| `get(id)` | `Promise<Client \| undefined>` | Resolves with the client whose `id` matches (ids come from `FetchEvent.clientId`, `ExtendableMessageEvent.source.id`, or an earlier `matchAll()`), or `undefined` when none does. |
| `matchAll(options)` | `Promise<Client[]>` | All clients matching `options`, most recently focused first. `options.type` is `'window'` (default), `'worker'`, `'sharedworker'`, or `'all'`; `options.includeUncontrolled` (default `false`) adds same-origin clients not controlled by this worker. |
| `openWindow(url)` | `Promise<WindowClient \| null>` | Opens a top-level browsing context at `url`. Resolves with the new client when it is same-origin, with `null` when it is cross-origin, and only succeeds while the worker is handling a user-initiated event such as `notificationclick`. |
| `claim()` | `Promise<void>` | Makes the active worker the controller of every in-scope client that is not controlled by another worker, firing `controllerchange` on each page's `navigator.serviceWorker`. |

Pages opened before the worker activated are not controlled by it; `claim()` is the only way to take them over without a reload. Combined with `skipWaiting()` it swaps the worker under an open page, which is why the update entry treats the pair as an explicit opt-in.

## Exceptions

The rejections are `DOMException` instances; `get()` has no rejection path in the specification.

- `InvalidAccessError` from `openWindow()`: the call did not happen during a user-initiated event. Chromium's message is `Not allowed to open a window.`; it applies to any call outside a `notificationclick` handler, including a `push` handler or a `setTimeout` callback inside `notificationclick`.
- `TypeError` from `openWindow()`: `url` is not a valid URL relative to the worker's location, or it is `about:blank`.
- `InvalidStateError` from `claim()`: the worker is not the registration's active worker, for example when called in `install`.

:::observed
In Chrome (English UI), calling `self.clients.openWindow('/chat/')` from a `push` event handler, or from `notificationclick` after awaiting an unrelated `fetch()` that outlives the user-activation window, rejects with `InvalidAccessError: Not allowed to open a window.`, the string constant in Chromium's `service_worker_clients.cc`. The same call issued synchronously inside `event.waitUntil()` in `notificationclick` resolves with a `WindowClient` whose `url` is the absolute URL and whose `focused` is `true`.
:::

## Examples

The first example runs in the service worker and is the canonical use of the interface; the second runs in the page and shows how to detect whether the worker controls it.

### Focusing an open window or opening one on notification click

Prefer an existing window over a new one: look for a client already at the target path, focus it, and only call `openWindow()` when none exists. Doing the work inside `waitUntil()` keeps the user-activation window open for `openWindow()`.

```js
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  event.waitUntil((async () => {
    const all = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
    const existing = all.find((c) => new URL(c.url).pathname === '/chat/');
    if (existing) {
      await existing.focus();
      existing.postMessage({ type: 'open-thread', id: event.notification.data.threadId });
      return;
    }
    const opened = await self.clients.openWindow(`/chat/?thread=${event.notification.data.threadId}`);
    if (opened === null) console.warn('Window opened cross-origin; cannot message it');
  })());
});
```

`includeUncontrolled: true` is needed because a tab opened from a notification while the previous worker was active may not be controlled by this one yet. `postMessage()` on a `WindowClient` arrives at the page's `navigator.serviceWorker` `message` event.

### Detecting control on the page and waiting for `claim()`

`claim()` runs in the worker, but the page is where its effect matters. `navigator.serviceWorker.controller` is `null` on the first load and after a hard reload; the page below enables its offline UI only once a controller exists, and otherwise behaves as an online-only site.

```js
function enableOfflineUi() {
  document.querySelector("#offline-badge").hidden = false;
}

if (!("serviceWorker" in navigator)) {
  // No service workers at all: online-only, nothing to wait for.
} else if (navigator.serviceWorker.controller) {
  enableOfflineUi();
} else {
  // Not controlled yet: claim() in the worker fires controllerchange here.
  navigator.serviceWorker.addEventListener("controllerchange", enableOfflineUi, { once: true });
}
```

Without `claim()` in the worker, `controllerchange` does not fire for this load and the badge stays hidden until the next navigation; that is the trade-off of leaving the default takeover behaviour in place.

## See also

- [Service Workers specification: Clients interface](https://w3c.github.io/ServiceWorker/#clients-interface) (w3c.github.io)
- [Clients: openWindow() method](https://developer.mozilla.org/en-US/docs/Web/API/Clients/openWindow) (developer.mozilla.org)
- [Service worker lifecycle](/reference/service-worker/lifecycle/)
- [Service worker update flow and skipWaiting()](/reference/service-worker/update-skipwaiting/)
- [Web Push](/reference/notifications/web-push/)
- [Notification actions and badges](/reference/notifications/notification-actions-badge/)