Skip to content

Capabilities · API

File System Access API: read and write local files

Published Updated

Limited availabilityNot supported in Chrome (Android), Safari (iOS), Safari (macOS), Firefox (Desktop)WICG draft

In one line: The File System Access API lets a web app open, edit, and save real files and folders on the user’s device through showOpenFilePicker, showSaveFilePicker, and showDirectoryPicker. It needs a secure context and transient user activation, writing requires read-write permission that prompts only when it is not already granted, and MDN marks it limited availability — not Baseline, because it does not work in some of the most widely-used browsers — so you need a fallback.

  • showOpenFilePicker() returns a promise fulfilled with an array of FileSystemFileHandle objects. Call handle.getFile() to read a File (a Blob) — .text(), .arrayBuffer(), or .stream() for the contents.
  • showSaveFilePicker() returns one FileSystemFileHandle pointed at a new or chosen file. Call handle.createWritable() to get a FileSystemWritableFileStream, write() to it, then close() to commit. Changes are typically written to a temporary file, and the file the handle represents is only replaced when the stream closes.
  • showDirectoryPicker() returns a FileSystemDirectoryHandle. Iterate its entries with for await (const [name, handle] of dir.entries()), and create or fetch children via getFileHandle(name, { create }) / getDirectoryHandle(name, { create }).

All three pickers are async and require transient user activation — the user has to interact with the page or a UI element — in a secure context (HTTPS or localhost). They reject with an AbortError if the user dismisses the prompt without selecting anything; if the user agent deems a selection too sensitive or dangerous it has discretion either to restart the picker or to reject with AbortError. A SecurityError follows a call blocked by the same-origin policy, or one not made from a user interaction such as a button press.

Everything below is one user gesture per picker, which is the shape the API demands:

let fileHandle;
openButton.addEventListener('click', async () => {
try {
// Destructure the one-element array the picker resolves to.
[fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
editor.value = await file.text();
} catch (error) {
// Dismissing the picker rejects with AbortError. That is a normal outcome,
// not a failure worth surfacing to the user.
if (error.name !== 'AbortError') throw error;
}
});
saveButton.addEventListener('click', async () => {
// No handle yet (new document)? Ask where to put it.
fileHandle ??= await window.showSaveFilePicker();
const writable = await fileHandle.createWritable();
await writable.write(editor.value);
await writable.close(); // buffered changes are committed here
});

One sharp edge in that save path: showSaveFilePicker() creates a new empty file, or clears the existing file the user selected, before it returns the handle. If the user points it at a file that already had contents, those contents are gone from the moment the picker resolves — not at close().

Detect the specific picker method you are about to call, and keep a path that works when it is absent. The pickers cannot be fully polyfilled, but each has a next-best approximation: showOpenFilePicker() with an <input type="file"> element, showSaveFilePicker() with an <a download="file_name"> element — which triggers a programmatic download and does not allow overwriting an existing file — and showDirectoryPicker() with the non-standard <input type="file" webkitdirectory> element. Chrome’s team publishes a library, browser-fs-access, that uses the API where possible and falls back to these options everywhere else.

if ('showOpenFilePicker' in self) {
// Real handles: the user can re-save over the file they opened.
const [handle] = await window.showOpenFilePicker();
render(await (await handle.getFile()).text());
} else {
// No pickers here. Fall back to <input type="file"> — the user can open a
// file, but saving becomes a download, not an in-place overwrite.
legacyFileInput.click();
}
  • Read access is granted when the user picks a file or folder. createWritable() checks write permission first, and the confirmation dialog appears only from the 'prompt' state — an already 'granted' or 'denied' state is returned as-is with nothing shown. Often it is already granted: showSaveFilePicker() returns a handle whose read-write permission should already be granted, and showDirectoryPicker({ mode: 'readwrite' }) can request it while picking.
  • Check current state without prompting via handle.queryPermission({ mode }); request it via handle.requestPermission({ mode }). Both resolve to 'granted', 'denied', or 'prompt', and transient user activation is needed only when the state is 'prompt' — from 'granted' or 'denied', requestPermission() returns that state before it checks for activation.
  • Grants are scoped to the origin and are not always persisted. The specification allows persistent permission for particularly trusted origins such as installed web apps, so on a later visit a handle may be granted, prompt, or denied. Check rather than assume either way.
  • The specification encourages user agents to restrict sensitive locations, giving system directories and the default downloads directory as its examples — while saying individual files inside that downloads directory should remain selectable, and separately permitting individual files and directories inside the user’s home directory. Treat “the user can always reach this path” as an assumption you do not get to make.

FileSystemFileHandle and FileSystemDirectoryHandle are structured-cloneable, so you can store them in IndexedDB and retrieve them on a later visit — the user does not have to re-pick the same file. The handle persists; the permission may or may not. After restoring a handle, call queryPermission(), and only if the state is 'prompt' call requestPermission() from a fresh user gesture before reading or writing. This pattern powers “recent files” lists in editors and IDEs on the web.

Pickers are not the only entry point. During an HTML drag-and-drop operation, DataTransferItem.getAsFileSystemHandle() returns a promise for a FileSystemFileHandle if the dragged item is a file, and for a FileSystemDirectoryHandle if it is a directory — so a dropped file becomes as editable as a picked one.

OPFS is the broadly-available part of this API: MDN marks it Baseline, widely available since March 2023. navigator.storage.getDirectory() returns a FileSystemDirectoryHandle rooted in a storage endpoint private to the page’s origin and not visible to the user like the regular file system, with no picker and no permission prompt. It provides files highly optimised for performance that offer in-place write access to their content, including synchronous access from a dedicated Web Worker via createSyncAccessHandle() — which is why it is the substrate for SQLite-in-the-browser and similar workloads. MDN contrasts it directly with the picker path, whose writes are not in-place, go through a temporary file, and carry a lot of security checks, making them fairly slow for large-scale updates such as SQLite database modifications.

Use OPFS when you need fast local storage; use the pickers when the user must see and own the actual files.

Decision question Recommended action Rationale
Need users to open and re-save their own files (editor, IDE)? Use the pickers + persist handles in IndexedDB. The user re-saves over the file they opened; “recent files” works across sessions.
Shipping to engines without the pickers? Fall back to <input type="file"> for open and an <a download> element for save. The API cannot be fully polyfilled; these are the documented next-best approximations.
Just need fast private storage (cache, DB, scratch space)? Use OPFS via navigator.storage.getDirectory(). Baseline since March 2023, no prompts, in-place writes, sync worker access.
Writing large files or streaming output? createWritable() and write() chunks, then close(). The file is replaced when the stream closes; avoids buffering it all in memory.
Restoring a saved handle on a later visit? queryPermission(), then requestPermission() inside a click handler if the state is 'prompt'. The handle persists; the grant is not always persisted.
  • Call pickers only from transient user activation — without it, expect a SecurityError. Serve over HTTPS/localhost too: outside a secure context the methods are simply not exposed.
  • Catch AbortError — the user dismissing the picker is normal, not an error.
  • Always close() the writable stream — changes are not written to the file it represents until then.
  • Persist handles in IndexedDB, but re-check with queryPermission() on return — the grant may already be there, or may need requestPermission().
  • Feature-detect the exact picker you call ('showOpenFilePicker' in self) and ship the <input type="file"> / <a download> fallback behind it.
  • Remember the save fallback is a download: it cannot overwrite the file the user opened, so design the UX around that.
  • Reach for OPFS when you need private, cross-engine, high-performance storage instead of user-visible files.
  • File handlers — registering your PWA as the app the OS opens a file type with, which is the other half of a real file-editing app.
  • File handling guide — the end-to-end walkthrough that puts pickers, handlers, and fallbacks together.
  • Storage buckets — finer-grained control over how origin storage is grouped and evicted.
  • File System Access compatibility — the per-browser dataset behind the table above.

Specifications

SpecificationStatus
File System Access APIWICG draft
  • Legend
  • Yes
  • Partial
  • Flag
  • No
  • Unknown
Browser / PlatformSupportVersionsConfidenceSourceNotes
Chrome (Android)No—mediumsource1
Chrome (Desktop)Yes86mediumsource—
Edge (Desktop)Yes86mediumsource—
Safari (iOS)No—mediumsource2
Safari (macOS)No—mediumsource3
Firefox (Desktop)No—mediumsource4
Samsung InternetNo—mediumsource5
  1. showOpenFilePicker/showSaveFilePicker are not exposed on Android.
  2. Only the origin-private file system (OPFS) is available, not the user-visible picker.
  3. No showOpenFilePicker; OPFS only.
  4. OPFS only; no user-visible file picker access.
  5. The picker methods (`showOpenFilePicker`, `showSaveFilePicker`, `showDirectoryPicker`) are desktop-only in Chromium; Chrome for Android has no implementation for Samsung Internet to mirror (MDN compatibility table, checked 2026-10-03).

Source data: /compatibility/file-system-access.json · Global usage: 37 % (StatCounter 2026-05)

Source: spec · MDN · Last verified 2026-06-24 · Confidence: medium (computed from sources)