# launch_handler manifest member

> The launch_handler member's client_mode decides whether launching an installed web app focuses an existing window, navigates it, or opens a new one; Chrome and Edge 110 implement it.

`launch_handler` tells the browser what to do with an installed web app's existing windows when
the app is launched again: from the OS launcher, a shortcut, a file association, a protocol
link, or a share. Its single property, `client_mode`, chooses between focusing a window that is
already open, navigating that window to the launch URL, or opening a new one.

Chrome 110, Edge 110, Samsung Internet 21.0, and Android WebView 110 implement the member (BCD
`html.manifest.launch_handler`). Firefox 157 and Safari 27 parse and drop it, so a launch there
always opens a new window or tab. The launch URL reaches the page through
`window.launchQueue`, which is what makes `focus-existing` usable at all.

## Member

- **Type**: object with one property, `client_mode`, which is a string or an array of
  strings. Allowed values: `auto`, `navigate-new`, `navigate-existing`, `focus-existing`.
- **Default**: `auto`. An absent member, a `client_mode` that is not a string or array, or an
  array in which no value is recognized all resolve to `auto`.
- **Example value**: `{ "client_mode": ["focus-existing", "auto"] }`.

What each mode does when a window is already open:

- `auto` leaves the choice to the browser. Chromium picks `navigate-existing` on Android,
  where one app instance is the norm, and `navigate-new` on desktop.
- `navigate-new` opens a new app window at the launch URL.
- `navigate-existing` brings the most recently used window to the front and navigates it to
  the launch URL.
- `focus-existing` brings that window to the front and does not navigate. The page receives the
  launch URL through `launchQueue` and decides what to do with it.

When no window is open, `focus-existing` and `navigate-existing` behave as `navigate-new`. In
an array, the browser takes the first value it recognizes, which is how a manifest can prefer
`focus-existing` and still name `auto` for engines that add new modes later.

The Chromium parser is strict about shape: a non-object value produces `launch_handler value
ignored, object expected.`, and an unknown string in `client_mode` produces `client_mode value
'<value>' ignored, unknown value.` (`manifest_parser.cc`, `ParseLaunchHandler`). The invalid
entry is skipped, not the whole member.

:::observed
Chrome 155 on macOS 26 (English UI), DevTools > Application > Manifest: a manifest carrying
`"launch_handler": { "client_mode": "focus" }` (a misspelling of `focus-existing`) adds
`client_mode value 'focus' ignored, unknown value.` under **Errors and warnings**, and relaunching
the installed app from the Dock opens a second window, which is the `auto` behaviour on
desktop. Correcting the value to `focus-existing` removes the line and the relaunch brings the
existing window forward instead.
:::

## Examples

The manifest side is one line; the interesting code is the `launchQueue` consumer that makes
`focus-existing` do something.

### Keeping a single editor window and routing launches into it

A document editor wants one window, with each launch switching to the requested document rather
than stacking windows. The manifest asks for `focus-existing`; the page consumes the launch
URL and updates its own state.

```json
{
  "name": "Draft",
  "start_url": "/",
  "display": "standalone",
  "launch_handler": { "client_mode": ["focus-existing", "auto"] },
  "file_handlers": [
    { "action": "/open", "accept": { "text/markdown": [".md"] } }
  ]
}
```

```js
if ('launchQueue' in window) {
  window.launchQueue.setConsumer(async (launchParams) => {
    const url = new URL(launchParams.targetURL);
    if (launchParams.files.length > 0) {
      const file = await launchParams.files[0].getFile();
      await openDocument(file);
      return;
    }
    const doc = url.searchParams.get('doc');
    if (doc) await openDocumentById(doc);
  });
} else {
  // No launchQueue: the browser navigated normally, so read the URL directly.
  const doc = new URL(location.href).searchParams.get('doc');
  if (doc) await openDocumentById(doc);
}
```

The consumer runs once for the launch that created the window and again for every later
launch that is routed into it. `launchParams.files` is an array of `FileSystemFileHandle`
objects when the launch came from a file association and empty otherwise.

### Detecting the API and falling back to normal navigation

`launchQueue` is the detection point for the whole mechanism. In a browser without it, every
launch is a plain navigation to `targetURL`, so the fallback is simply to read `location`.
The helper below gives the rest of the app a single callback either way.

```js
export function onLaunch(handler) {
  if ('launchQueue' in window) {
    window.launchQueue.setConsumer((params) => handler(new URL(params.targetURL)));
  } else {
    handler(new URL(location.href));
  }
}

onLaunch((url) => {
  if (url.pathname === '/share') showShareSheet(url.searchParams);
});
```

Register the consumer before the first `await` in the module: Chromium queues launch
parameters until a consumer exists, but a consumer set late still fires, so nothing is lost,
only delayed.

### Preferring a fresh window on desktop and an existing one on mobile

A news reader wants each desktop launch in its own window but a single window on a phone.
`auto` already encodes that split in Chromium, so the manifest states it explicitly and lets
the browser choose.

```json
{
  "launch_handler": { "client_mode": "auto" }
}
```

Spelling `auto` out is equivalent to omitting the member; the value exists so a manifest that
is edited later has an obvious place to change.

## See also

- [file_handlers manifest member](/reference/manifest/file-handlers/)
- [protocol_handlers manifest member](/reference/manifest/protocol-handlers/)
- [share_target manifest member](/reference/manifest/share-target/)
- [start_url manifest member](/reference/manifest/start-url/)
- [Web App Launch Handler API: launch_handler member](https://wicg.github.io/web-app-launch/#launch_handler-member) (wicg.github.io)
- [Control how your app is launched](https://developer.chrome.com/docs/web-platform/launch-handler) (developer.chrome.com)
- [LaunchQueue](https://developer.mozilla.org/en-US/docs/Web/API/LaunchQueue) (developer.mozilla.org)