Skip to content

File System Access & OPFS

The browser provides two distinct file-writing surfaces:

  1. 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.
  2. 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.

The picker functions show the native OS file dialog. They must be called from a user-gesture handler.

// Open a file chosen by the user
async 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 file
async 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.

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 OPFS
const root = await navigator.storage.getDirectory();
// Create or open a file handle
const fileHandle = await root.getFileHandle('notes.txt', { create: true });
// Write to it
const writable = await fileHandle.createWritable();
await writable.write('Hello from OPFS!');
await writable.close();
// Read it back
const file = await fileHandle.getFile();
const text = await file.text();
console.log(text); // 'Hello from OPFS!'
const root = await navigator.storage.getDirectory();
// Create a sub-directory
const subDir = await root.getDirectoryHandle('drafts', { create: true });
// List entries
for await (const [name, handle] of subDir.entries()) {
console.log(handle.kind, name);
}
Browser Storage
OptionBenefitCost
File System Access APIAccesses a real file on disk that the user picked themselves — good for import/export where the user sees and manages the fileRequires a user gesture every time, and isn’t supported outside Chromium-based browsers
OPFSI/O speed close to native, since it’s a sandboxed virtual file system — full read/write access with no user gesture neededMore 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 lessonsLower throughput than OPFS for large files or frequent writes
  • Assuming the File System Access API works in every browser like OPFS does — showOpenFilePicker/showSaveFilePicker are fully supported only in Chromium-based browsers; Firefox and Safari don’t support them yet.
  • Calling showOpenFilePicker outside a user-gesture handler — for example inside useEffect or a non-click event — then wondering why it throws SecurityError.
  • 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 though write() already resolved.
Why does showOpenFilePicker require a user gesture?
What does navigator.storage.getDirectory() return?
After calling fileHandle.createWritable() and writing data, what must you do before the data is visible to future reads?
Which of the following correctly describes OPFS storage persistence?