Skip to content

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.

  • showOpenFilePicker() returns 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. Writes go to a temp file and atomically replace the target on close.
  • 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, 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).

  • Read access is granted when the user picks a file or folder. Writing prompts again — the first createWritable() (or an explicit requestPermission({ mode: 'readwrite' })) triggers a permission dialog.
  • Check current state without prompting via handle.queryPermission({ mode }); request it (must be inside a user gesture) via handle.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.

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.

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 / PlatformSupportSinceConfidenceSourceNotes
Chrome (Android)❌ nohighrefshowOpenFilePicker/showSaveFilePicker are not exposed on Android.
Chrome (Desktop)✅ yes86highref
Edge (Desktop)✅ yes86highref
Safari (iOS)❌ nomediumrefOnly the origin-private file system (OPFS) is available, not the user-visible picker.
Safari (macOS)❌ nomediumrefNo showOpenFilePicker; OPFS only.
Firefox (Desktop)❌ nomediumrefOPFS only; no user-visible file picker access.
Samsung Internet❌ nomediumref

Source: spec · MDN · Last verified 2026-06-24 · Confidence: high

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.
  • 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/requestPermission on 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.