# Handle files

> Register an installed PWA as an OS-level file handler: declare file_handlers, consume launches with launchQueue, and fall back when the API is missing.

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

At the end of this guide a desktop user who double-clicks a `.csv` file, or picks your app
under "Open with", lands in your installed PWA with that file already loaded, and a user on
Android, Firefox, or Safari gets the same import through an in-app file picker. The OS-level
association comes from the `file_handlers` manifest member, read when the PWA is installed;
reading the file happens in page JavaScript through `window.launchQueue`.

You need an installable PWA (see [Getting started](/guides/getting-started/)), Chrome or
Edge 102 or later on Windows, macOS, Linux, or ChromeOS, and a file type to own.

## Declare the handler in the manifest

Each entry in `file_handlers` needs an `action` URL inside the app's navigation scope and an
`accept` map from MIME type to file extensions (MDN marks both as required). Chrome also
reads an optional `icons` array, which lets the OS show a file-type icon instead of the app
icon, and `launch_type`:

```json
{
  "file_handlers": [
    {
      "action": "/open",
      "accept": { "text/csv": [".csv"] },
      "icons": [
        { "src": "/icons/csv-file.png", "sizes": "256x256", "type": "image/png" }
      ],
      "launch_type": "single-client"
    }
  ]
}
```

`launch_type` defaults to `"single-client"`: opening several matching files at once produces
one launch whose `LaunchParams.files` holds every file. With `"multiple-clients"` Chrome
launches the app once per file and each launch's `files` array has one element. Reinstall
the PWA after changing this member; the OS association is written at install time, and Chrome
resets the file-handling permission whenever `file_handlers` changes.

<CompatTable feature="manifest-file-handlers" />

Chrome's capability article states that File Handling is "limited to desktop operating
systems", so the handler does nothing on Android even though the manifest is accepted there.

## Consume the launch in the page

When the OS opens a file with the app, Chrome navigates to `action` and enqueues a
`LaunchParams` object on `window.launchQueue`; Chrome's launch-handler documentation
describes the queue as holding launches "until they are handled by the specified consumer",
so a consumer registered after the page has booted still receives the launch. Register it once,
at top level, before any async startup work:

```js
window.launchQueue.setConsumer(async (launchParams) => {
  if (!launchParams.files || launchParams.files.length === 0) return;
  for (const handle of launchParams.files) {
    const file = await handle.getFile();
    renderCsv(file.name, await file.text());
  }
});
```

Each entry in `files` is a file handle rather than a `File`, which is why the consumer calls
`getFile()` before reading.

## Detect support and fall back to a picker

Chrome's article gives the detection as `'launchQueue' in window && 'files' in
LaunchParams.prototype`; the second half matters because a browser could ship `launchQueue`
for `launch_handler` navigation without the `files` member. When the check fails, show a
standard file input so the feature still exists, without OS integration:

```js
function supportsFileHandling() {
  return 'launchQueue' in window && 'files' in LaunchParams.prototype;
}

const picker = document.querySelector('#open-file-input'); // <input type="file" accept=".csv">

if (supportsFileHandling()) {
  window.launchQueue.setConsumer(async (launchParams) => {
    for (const handle of launchParams.files ?? []) {
      const file = await handle.getFile();
      renderCsv(file.name, await file.text());
    }
  });
} else {
  picker.hidden = false;
  picker.addEventListener('change', async () => {
    const [file] = picker.files;
    if (file) renderCsv(file.name, await file.text());
  });
}
```

Keep the picker reachable on desktop too: a user who has not installed the app, or who opens
the app first and then wants a file, has no OS launch to rely on.

## Verify the association

Install the PWA, then open a `.csv` from the OS file manager. The first launch shows Chrome's
permission prompt before the app can read the file; grant it and the consumer runs with one
entry in `files`.

:::observed
Chrome's File Handling article (developer.chrome.com, read 2026-10-03) documents that the
permission prompt is shown "before a PWA can view a file" and reappears on each launch until
the user chooses **Allow** or **Block**, or "ignores the prompt three times (after which
Chromium will embargo and block this permission)". Three dismissed launches during testing
therefore silently disable the feature for that origin; reset it from the site's permission
settings rather than reinstalling.
:::

## See also

- [`file_handlers` manifest member](/reference/manifest/file-handlers/)
- [`file_handlers` compatibility](/compatibility/manifest-file-handlers/)
- [Receive shared content (share target)](/guides/share-target/)
- [File System Access](/reference/capabilities/file-system-access/)
- [Let installed web applications be file handlers](https://developer.chrome.com/docs/capabilities/web-apis/file-handling) (developer.chrome.com)
- [`file_handlers`](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/file_handlers) (developer.mozilla.org)