JavaScript File System Access Table
| Piece | What it does | Field note |
|---|---|---|
showOpenFilePicker() | Pick + HANDLE | Returns a FileSystemFileHandle - re-readable without re-prompting |
handle.getFile() | Read via handle | A fresh File each call - the content may have changed on disk |
showSaveFilePicker() | Pick where to save | createWritable() writes - real saves, not download copies |
showDirectoryPicker() | A whole folder handle | Iterate entries - editors that open projects |
Handles are serializable | Persist in IndexedDB | Re-prompt once on restore - then the same file again |
Chromium-only | The support reality | Safari/Firefox: no - feature-detect, fall back to input+download |
vs File API input | One-shot vs handle | input gives a File copy; handles give a LIVING file |
Permission per write | The ask model | Read once; writes re-confirm - user keeps control |
The File System Access API upgrades one-shot File picking to LIVING FILE HANDLES: showOpenFilePicker returns a handle you can re-read, write to via createWritable, and even persist in IndexedDB - the editor that reopens your file next session is this API.
Bottom line: handles turn 'upload a copy' into 'edit this file' - the difference between a form and an application. The trade: Chromium-only, so the input-plus-download pattern remains the universal fallback, behind a feature-detect.
The honest part: the permission model is graduated - the read prompt covers reads, but writes re-confirm (the browser asks again on first write), keeping the user in control of the destructive-capability boundary even after they picked the file.
How to use
- Open with a handle: const [handle] = await showOpenFilePicker(); const file = await handle.getFile() - keep the handle for later saves.
- Save in place: const writable = await handle.createWritable(); await writable.write(data); await writable.close() - the real file updates, no download.
- Persist the handle: store it in IndexedDB, re-prompt once on restore with requestPermission({mode: 'readwrite'}) - the same file again next session.
Frequently asked questions
What does a FileSystemHandle give that a File from an input does not?
Liveness and capability. The input File is a SNAPSHOT: a copy of the bytes at pick time - save means downloading a new file, and edits to the original on disk are invisible. A handle is a REFERENCE to the living file: getFile() re-reads fresh bytes on demand (picking up external changes), createWritable() writes back to the real location, and queryPermission/requestPermission manage ongoing capability. The architectural consequence: apps built on handles behave like desktop editors (open, edit, save in place, reopen later) while input-built apps remain import-export tools. Both have their place; handles are the application-shaped one.
How does handle persistence across sessions work?
IndexedDB stores handles - they are structured-cloneable objects, so an editor saves the handle alongside the document metadata. On restore, the handle comes back but its permissions do NOT: requestPermission({mode: 'readwrite'}) re-asks once (a lighter prompt referencing the known file), and then the same file is live again. The UX win: 'reopen report.md' instead of a file picker treasure hunt. The caveats: the user can always deny on restore (handle the rejection as 'open another file'), and persisted handles can be revoked from browser site settings - the permission model still belongs to the user.
Why is write access re-confirmed separately from read?
The destructive boundary is different. Reading a file exposes data; writing REPLACES it - the irreversible action. So the API graduates: the open prompt grants reads of the picked file, and the first createWritable triggers a separate save confirmation - the user approves writing to THAT file. This is why scripts cannot silently corrupt a picked file even after legitimate reads. For apps, design for the ask: it fires once per handle per session typically, and queryPermission({mode: 'readwrite'}) lets you check before attempting so the write flow can explain itself before requesting.
How should a product handle the Chromium-only reality?
Two implementations behind one feature-detect. if (window.showOpenFilePicker) - the handle path (edit-in-place, reopen, real saves); else - the classic path: <input type=file> for open, download via blob URL or the File System Access-less save for export. The product stays functional everywhere and gains powers on Chromium. The honesty rule: do not advertise 'save' when the fallback downloads copies - label it 'export' in fallback mode, because the mental models differ (in-place edit vs new file in Downloads). Many apps shipped for years with just the fallback; the API is the upgrade tier, not the baseline.