File System Access & OPFS
Two ways to write files
Section titled “Two ways to write files”The browser provides two distinct file-writing surfaces:
- File System Access API (
showOpenFilePicker,showSaveFilePicker) — lets users pick real files from their local file system. Requires a user gesture (button click). Not available in all browsers. - Origin Private File System (OPFS) — a sandboxed, origin-scoped virtual file system. No user gesture, no file picker, fully programmatic. Works in workers. Quota-managed like IndexedDB.
File System Access API
Section titled “File System Access API”The picker functions show the native OS file dialog. They must be called from a user-gesture handler.
// Open a file chosen by the userasync function openFile() { // showOpenFilePicker must be called inside a user gesture (click, etc.) const [fileHandle] = await window.showOpenFilePicker(); const file = await fileHandle.getFile(); const text = await file.text(); console.log('File contents:', text.slice(0, 200));}
// Save / overwrite a fileasync function saveFile(content) { const handle = await window.showSaveFilePicker({ suggestedName: 'output.txt', types: [{ description: 'Text file', accept: { 'text/plain': ['.txt'] } }], }); const writable = await handle.createWritable(); await writable.write(content); await writable.close();}Because showOpenFilePicker and showSaveFilePicker require a user gesture, they cannot be run autonomously in the StorageRunner above. The OPFS demo below is fully runnable.
Origin Private File System (OPFS)
Section titled “Origin Private File System (OPFS)”OPFS gives every origin a private directory tree. You do not need a user gesture — just call navigator.storage.getDirectory().
// Get the root of the OPFSconst root = await navigator.storage.getDirectory();
// Create or open a file handleconst fileHandle = await root.getFileHandle('notes.txt', { create: true });
// Write to itconst writable = await fileHandle.createWritable();await writable.write('Hello from OPFS!');await writable.close();
// Read it backconst file = await fileHandle.getFile();const text = await file.text();console.log(text); // 'Hello from OPFS!'Directory navigation
Section titled “Directory navigation”const root = await navigator.storage.getDirectory();
// Create a sub-directoryconst subDir = await root.getDirectoryHandle('drafts', { create: true });
// List entriesfor await (const [name, handle] of subDir.entries()) { console.log(handle.kind, name);}Runnable: OPFS write and read
Section titled “Runnable: OPFS write and read”Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
| File System Access API | Accesses a real file on disk that the user picked themselves — good for import/export where the user sees and manages the file | Requires a user gesture every time, and isn’t supported outside Chromium-based browsers |
| OPFS | I/O speed close to native, since it’s a sandboxed virtual file system — full read/write access with no user gesture needed | More limited browser support than IndexedDB/localStorage, and the user can’t see the file directly since it isn’t a real file in the OS |
| IndexedDB (storing a blob generally) | Widest browser support, with the transaction model already familiar from earlier lessons | Lower throughput than OPFS for large files or frequent writes |
Common mistakes
Section titled “Common mistakes”- Assuming the File System Access API works in every browser like OPFS does —
showOpenFilePicker/showSaveFilePickerare fully supported only in Chromium-based browsers; Firefox and Safari don’t support them yet. - Calling
showOpenFilePickeroutside a user-gesture handler — for example insideuseEffector a non-click event — then wondering why it throwsSecurityError. - Forgetting to call
writable.close()after writing a file in OPFS — the data never gets flushed and won’t show up on the next read, even thoughwrite()already resolved.