File System Access API: reading and writing local files
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 a user
gesture, every write is gated by an explicit permission prompt, and it ships only in
Chromium browsers — so Safari and Firefox need a fallback.
Pickers and handles
Section titled “Pickers and handles”showOpenFilePicker()returns 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. Writes go to a temp file and atomically replace the target on close.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, reject with an AbortError if the user cancels, and must
be called from a user gesture (click, keypress) in a secure context (HTTPS or
localhost).
The permission model
Section titled “The permission model”- Read access is granted when the user picks a file or folder. Writing prompts again —
the first
createWritable()(or an explicitrequestPermission({ mode: 'readwrite' })) triggers a permission dialog. - Check current state without prompting via
handle.queryPermission({ mode }); request it (must be inside a user gesture) viahandle.requestPermission({ mode }). Both resolve to'granted','denied', or'prompt'. - Grants are scoped to the origin and are not permanent — they typically last for the tab/session and reset when the page is fully closed, so re-prompting on return is expected.
- Certain sensitive locations (system folders, the download directory in some cases) are blocked outright.
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, but the permission does not: after
restoring a handle, call queryPermission() and, if needed, requestPermission() from a
fresh user gesture before reading or writing. This pattern powers “recent files” lists in
editors and IDEs on the web.
Origin Private File System (OPFS)
Section titled “Origin Private File System (OPFS)”OPFS is the broadly-available subset of this API. navigator.storage.getDirectory()
returns a FileSystemDirectoryHandle rooted in a private, origin-scoped sandbox — no
picker, no permission prompt, not visible in the user’s file manager. It is available in
all modern engines including Safari and Firefox, supports high-performance
synchronous access from Web Workers via createSyncAccessHandle(), and is ideal for
SQLite-in-the-browser, caches, and large working data. Use OPFS when you need fast local
storage; use the pickers when the user must see and own the actual files.
Browser & ecosystem support
Section titled “Browser & ecosystem support”| Browser / Platform | Support | Since | Confidence | Source | Notes |
|---|---|---|---|---|---|
| Chrome (Android) | ❌ no | — | high | ref | showOpenFilePicker/showSaveFilePicker are not exposed on Android. |
| Chrome (Desktop) | ✅ yes | 86 | high | ref | — |
| Edge (Desktop) | ✅ yes | 86 | high | ref | — |
| Safari (iOS) | ❌ no | — | medium | ref | Only the origin-private file system (OPFS) is available, not the user-visible picker. |
| Safari (macOS) | ❌ no | — | medium | ref | No showOpenFilePicker; OPFS only. |
| Firefox (Desktop) | ❌ no | — | medium | ref | OPFS only; no user-visible file picker access. |
| Samsung Internet | ❌ no | — | medium | ref | — |
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. | True in-place editing; “recent files” works across sessions. |
| Targeting Safari or Firefox too? | Fall back to <input type="file"> for open and an <a download> blob URL for save. |
Pickers are Chromium-only; this covers all engines with a graceful UX. |
| Just need fast private storage (cache, DB, scratch space)? | Use OPFS via navigator.storage.getDirectory(). |
Works everywhere, no prompts, sync worker access for performance. |
| Writing large files or streaming output? | createWritable() and write() chunks, then close(). |
Atomic replace on close; avoids buffering the whole file in memory. |
| Restoring a saved handle on a later visit? | queryPermission(), then requestPermission() inside a click handler. |
The handle persists but the grant does not; re-prompt is required. |
Practical checklist
Section titled “Practical checklist”- Call pickers only from a user gesture and only over HTTPS/
localhost. - Catch
AbortError— the user cancelling the picker is normal, not an error. - Always
close()the writable stream so the atomic write commits. - Persist handles in IndexedDB, but re-verify permission with
query/requestPermissionon return. - Provide an
<input type="file">+ download-link fallback for Safari and Firefox. - Reach for OPFS when you need private, cross-engine, high-performance storage instead of user-visible files.