Skip to content

The Clients API: a service worker's access to client objects

In one line: The Clients interface, accessed as self.clients inside a service worker, provides access to Client objects (windows, workers) matching the query options passed in — with matchAll({ includeUncontrolled: true }), this can include clients that share the same storage key/origin but are not controlled by, or associated with the registration of, this service worker.

This feature is only available in service workers.

// Inside a service worker
self.clients;
Method Returns Purpose
Clients.get(id) Promise<Client | undefined> Returns a Client matching a given id, or undefined if no client matches.
Clients.matchAll(options) Promise<Array<Client>> Returns an array of Client objects. By default only controlled clients are returned; an options argument allows control over the types of clients returned, including uncontrolled ones.
Clients.openWindow(url) Promise<WindowClient | null> Opens a new browser window for a given URL, resolving to null if the platform cannot return a same-origin WindowClient for it.
Clients.claim() Promise<void> Allows an active service worker to set itself as the controller for all clients within its scope.

Finding an already-open chat window, focusing it, or opening a new one, then messaging it:

addEventListener("notificationclick", (event) => {
event.waitUntil(
(async () => {
const allClients = await clients.matchAll({
includeUncontrolled: true,
});
let chatClient;
// Let's see if we already have a chat window open:
for (const client of allClients) {
const url = new URL(client.url);
if (url.pathname === "/chat/") {
// Excellent, let's use it!
client.focus();
chatClient = client;
break;
}
}
// If we didn't find an existing chat window,
// open a new one:
chatClient ??= await clients.openWindow("/chat/");
// Message the client:
chatClient.postMessage("New chat messages!");
})(),
);
});

The Clients interface is Baseline widely available: it is well established and works across many devices and browser versions, and has been available across browsers since April 2018.

  • Use clients.matchAll() with includeUncontrolled when a service worker needs to find windows it does not yet control — by default matchAll() only returns controlled clients.
  • Handle clients.get(id) resolving to undefined and clients.openWindow(url) resolving to null when no matching client can be returned.
  • Call clients.claim() in the service worker’s activate handler when it should take control of existing clients immediately, rather than waiting for the next navigation.
  • Use clients.openWindow() only in response to a user gesture such as a notification click, matching the pattern shown above.
  • Navigation preload — another service worker capability exposed via a similarly scoped API