Capabilities · API
File System Access API: read and write local files
Published Updated
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.
Pickers and handles
Section titled “Pickers and handles”showOpenFilePicker()returns a promise fulfilled with an array ofFileSystemFileHandleobjects. Callhandle.getFile()to read aFile(aBlob) —.text(),.arrayBuffer(), or.stream()for the contents.showSaveFilePicker()returns oneFileSystemFileHandlepointed at a new or chosen file. Callhandle.createWritable()to get aFileSystemWritableFileStream,write()to it, thenclose()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 aFileSystemDirectoryHandle. Iterate its entries withfor await (const [name, handle] of dir.entries()), and create or fetch children viagetFileHandle(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.
A minimal open-edit-save round trip
Section titled “A minimal open-edit-save round trip”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().
Feature detection and fallback
Section titled “Feature detection and fallback”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();}The permission model
Section titled “The permission model”- 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, andshowDirectoryPicker({ mode: 'readwrite' })can request it while picking. - Check current state without prompting via
handle.queryPermission({ mode }); request it viahandle.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.
Persisting handles across sessions
Section titled “Persisting handles across sessions”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.
Handles from drag and drop
Section titled “Handles from drag and drop”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.
Origin Private File System (OPFS)
Section titled “Origin Private File System (OPFS)”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 framework
Section titled “Decision framework”| 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. |
Practical checklist
Section titled “Practical checklist”- Call pickers only from transient user activation — without it, expect a
SecurityError. Serve over HTTPS/localhosttoo: 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 needrequestPermission(). - 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.
Where to go next
Section titled “Where to go next”- 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
| Specification | Status |
|---|---|
| File System Access API | WICG draft |
- Legend
- Yes
- Partial
- Flag
- No
- Unknown
| Browser / Platform | Support | Versions | Confidence | Source | Notes |
|---|---|---|---|---|---|
| Chrome (Android) | No | — | medium | source | 1 |
| Chrome (Desktop) | Yes | 86 | medium | source | — |
| Edge (Desktop) | Yes | 86 | medium | source | — |
| Safari (iOS) | No | — | medium | source | 2 |
| Safari (macOS) | No | — | medium | source | 3 |
| Firefox (Desktop) | No | — | medium | source | 4 |
| Samsung Internet | No | — | medium | source | 5 |
- showOpenFilePicker/showSaveFilePicker are not exposed on Android.
- Only the origin-private file system (OPFS) is available, not the user-visible picker.
- No showOpenFilePicker; OPFS only.
- OPFS only; no user-visible file picker access.
- 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).