# File System Access API

> showOpenFilePicker(), showSaveFilePicker(), and showDirectoryPicker() return handles to real files. Every option, the exceptions they reject with, fallbacks.

The File System Access API lets a page open, edit, and save real files and folders on the user's device: `showOpenFilePicker()`, `showSaveFilePicker()`, and `showDirectoryPicker()` return `FileSystemFileHandle` and `FileSystemDirectoryHandle` objects that stay valid after the picker closes, can be stored in IndexedDB, and write back to the same file through `createWritable()`. The handle interfaces themselves are defined in the WHATWG File System Standard and are shared with the Origin Private File System.

The pickers are Chromium-only: Chrome 86 and Edge 86 on desktop. Firefox 111 and Safari 15.2 implement the handle interfaces for OPFS but expose no picker, and Safari added `createWritable()` only in Safari 26 (BCD `api.Window.showOpenFilePicker`, `api.FileSystemFileHandle.createWritable`). `queryPermission()` and `requestPermission()` on handles are Chrome 86 only.

## Syntax

```js
window.showOpenFilePicker()
window.showOpenFilePicker(options)

window.showSaveFilePicker()
window.showSaveFilePicker(options)

window.showDirectoryPicker()
window.showDirectoryPicker(options)
```

`showOpenFilePicker()` returns a `Promise<sequence<FileSystemFileHandle>>` (one element unless `multiple` is `true`); `showSaveFilePicker()` a `Promise<FileSystemFileHandle>`; `showDirectoryPicker()` a `Promise<FileSystemDirectoryHandle>`. All three are `[SecureContext]` and require transient user activation. A save-picker handle comes back with read-write permission already granted, and `showSaveFilePicker()` empties the chosen file at the moment the promise resolves, not when you later `close()` the writable stream.

## Parameters

Each picker takes one optional `options` dictionary. `OpenFilePickerOptions` and `SaveFilePickerOptions` extend the shared `FilePickerOptions`; `DirectoryPickerOptions` is separate.

| Member | Type | Required | Applies to | Description |
|---|---|---|---|---|
| `types` | `sequence<FilePickerAcceptType>` | No | open, save | Filters the user can pick from. Each entry has a `description` (`USVString`, default `""`) and an `accept` record mapping a MIME type to one suffix or a list of suffixes, for example `{ "text/plain": [".txt", ".md"] }`. |
| `excludeAcceptAllOption` | `boolean` | No | open, save | Default `false`. When `true`, the "All files" filter is omitted; when `types` is empty it is added regardless. |
| `id` | `DOMString` | No | all | Up to 32 characters of letters, digits, `_` or `-`. The browser remembers the last directory used with this id per origin and starts there next time. |
| `startIn` | `WellKnownDirectory` or `FileSystemHandle` | No | all | Where to start: one of `"desktop"`, `"documents"`, `"downloads"`, `"music"`, `"pictures"`, `"videos"`, or a handle from an earlier pick. A remembered `id` directory takes precedence. |
| `multiple` | `boolean` | No | open | Default `false`. When `true`, the user may select any number of files. |
| `suggestedName` | `USVString?` | No | save | Pre-fills the file name; the browser may sanitise or ignore a name it considers dangerous. |
| `mode` | `FileSystemPermissionMode` | No | directory | `"read"` (default) or `"readwrite"`. With `"readwrite"` the permission prompt covers writing, so the picked directory needs no second prompt. |

Suffixes in `accept` must start with `.`, must not end with `.`, must be at most 16 characters, and may contain only valid suffix code points.

## Exceptions

The pickers reject in the order below; the `SecurityError` and `TypeError` cases are checked before any dialog appears.

| Exception | Condition |
|---|---|
| `SecurityError` | The document has an opaque origin (a sandboxed iframe); or its origin differs from the top-level origin (a cross-origin iframe); or the window has no transient user activation. |
| `TypeError` | A key in `accept` is not a valid MIME type or carries parameters; a suffix breaks the rules above; `types` plus the all-files option leaves no filter at all; `id` is longer than 32 characters or contains other characters. |
| `AbortError` | The user dismissed the dialog without choosing; or the browser judged the selection too sensitive (system folders, the downloads directory as a whole) and chose to reject rather than reopen the dialog; or, for `showDirectoryPicker()`, the permission request did not end in `"granted"`. |

Later operations on the returned handles raise the File System Standard's errors instead: `NotAllowedError` when permission is not granted, `NotFoundError` when the file was deleted or moved, `NoModificationAllowedError` when another writable or sync access handle holds a lock.

:::observed
Chrome rejects `showOpenFilePicker()` called outside a user-activation handler with `SecurityError: Must be handling a user gesture to show a file picker.`, a call from a cross-origin iframe with `SecurityError: Cross origin sub frames aren't allowed to show a file picker.`, and `{ types: [{ accept: { "image/png": ["png"] } }] }` with `TypeError: Extension 'png' must start with '.'.` The strings are thrown in Chromium's [`global_file_system_access.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/file_system_access/global_file_system_access.cc) (chromium.googlesource.com).
:::

## Examples

Each example feature-detects the exact method it is about to call; `"showOpenFilePicker" in window` is `false` in Firefox and Safari even though both expose `FileSystemFileHandle` for OPFS.

### Opening, editing, and saving a text file, with an input and download fallback

Where the pickers exist, the user re-saves over the file they opened. Where they do not, `<input type="file">` reads a file and an `<a download>` click writes a new copy; the fallback cannot overwrite the original, so the UI should say "Download" rather than "Save".

```js
let fileHandle = null;
const editor = document.querySelector("#editor");
const legacyInput = document.querySelector("#legacy-file");
const supported = "showOpenFilePicker" in window;

document.querySelector("#open").addEventListener("click", async () => {
  if (!supported) {
    legacyInput.click();
    return;
  }
  try {
    [fileHandle] = await window.showOpenFilePicker({
      types: [{ description: "Text", accept: { "text/plain": [".txt", ".md"] } }],
    });
    editor.value = await (await fileHandle.getFile()).text();
  } catch (err) {
    if (err.name !== "AbortError") throw err; // dismissing the dialog is normal
  }
});

legacyInput.addEventListener("change", async () => {
  editor.value = await legacyInput.files[0].text();
});

document.querySelector("#save").addEventListener("click", async () => {
  if (!supported) {
    const link = document.createElement("a");
    link.href = URL.createObjectURL(new Blob([editor.value], { type: "text/plain" }));
    link.download = "document.txt";
    link.click();
    URL.revokeObjectURL(link.href);
    return;
  }
  fileHandle ??= await window.showSaveFilePicker({ suggestedName: "document.txt" });
  const writable = await fileHandle.createWritable();
  await writable.write(editor.value);
  await writable.close(); // the file on disk changes only here
});
```

Writes go to a temporary file and replace the target when `close()` resolves; a tab closed before `close()` leaves the original untouched.

### Restoring a handle from IndexedDB and re-checking permission

Handles are structured-cloneable, so a "recent files" list can store them. The permission may or may not survive; call `queryPermission()` first and `requestPermission()` only from a click when the state is `"prompt"`.

```js
async function reopen(db, button) {
  if (!("showOpenFilePicker" in window)) return null;

  const handle = await db.get("recent", "last"); // a file handle stored by an earlier pick
  if (!handle) return null;

  const mode = { mode: "readwrite" };
  let state = await handle.queryPermission(mode);
  if (state === "prompt") {
    await new Promise((resolve) => button.addEventListener("click", resolve, { once: true }));
    state = await handle.requestPermission(mode);
  }
  return state === "granted" ? handle : null;
}
```

A `null` result sends the caller back to the picker; a `"denied"` state stays denied until the user changes it in site settings.

## See also

- [Manifest file_handlers](/reference/manifest/file-handlers/), the other half of a file-editing app: registering the PWA as the opener for a file type
- [Handle files](/guides/file-handling/)
- [Origin Private File System (OPFS)](/reference/storage/opfs/), the cross-engine, prompt-free part of the same handle API
- [Storage Buckets API](/reference/capabilities/storage-buckets/)
- [File System Access: showOpenFilePicker() method](https://wicg.github.io/file-system-access/#api-showopenfilepicker) (wicg.github.io)
- [File System Standard: FileSystemFileHandle interface](https://fs.spec.whatwg.org/#api-filesystemfilehandle) (fs.spec.whatwg.org)
- [The File System Access API: simplifying access to local files](https://developer.chrome.com/docs/capabilities/web-apis/file-system-access) (developer.chrome.com)