File System Access API
The File System Access API gives web applications a way to interact with files on the user’s local device. Before this API, web apps could only read files through <input type="file"> dialogs, and writing required downloading files to the browser’s download folder. Now you can open a file, make changes, and save them directly — with the user’s permission.
The API is built around handles. A handle is a reference to a file or directory that the user has granted you access to. The browser manages the actual file access, so malicious scripts cannot read arbitrary files.
Opening Files with a Picker
The showOpenFilePicker() method shows the system’s native file picker and returns handles to the selected files:
const [fileHandle] = await window.showOpenFilePicker();
Destructuring the result gives you a FileSystemFileHandle. You can configure the picker with options to restrict what the user can select:
const pickerOpts = {
types: [
{
description: "Images",
accept: {
"image/*": [".png", ".gif", ".jpeg", ".jpg"]
}
}
],
excludeAcceptAllOption: true,
multiple: false
};
const [fileHandle] = await window.showOpenFilePicker(pickerOpts);
Setting multiple: false (or omitting it) returns a single handle. Setting multiple: true lets the user pick several files at once:
const handles = await window.showOpenFilePicker({ multiple: true });
Once you have a handle, call getFile() to read its contents:
const fileHandle = await window.showOpenFilePicker()[0];
const file = await fileHandle.getFile();
console.log(file.name, file.size, file.lastModified);
const contents = await file.text();
getFile() returns a File object — the same type you’d get from an <input type="file">. You can use .text(), .arrayBuffer(), or .stream() on it.
Saving Files with a Picker
showSaveFilePicker() opens a save dialog and returns a handle you can write to:
const handle = await window.showSaveFilePicker();
const writable = await handle.createWritable();
await writable.write("Hello, file system!");
await writable.close();
createWritable() returns a FileSystemWritableFileStream, which is a WritableStream with extra convenience methods. You can write plain data directly:
await writable.write("Plain text content");
await writable.write(myBlob);
await writable.write(myArrayBuffer);
You can also do positioned operations:
// Write at a specific byte offset
await writable.write({ type: "write", position: 0, data: updatedContent });
// Move the cursor to a position
await writable.write({ type: "seek", position: 10 });
// Truncate the file to a specific size
await writable.write({ type: "truncate", size: 100 });
The file is only written to disk when you call close().
Opening a Directory
showDirectoryPicker() gives you a FileSystemDirectoryHandle for navigating a directory tree:
const dirHandle = await window.showDirectoryPicker();
From a directory handle, you can:
getFileHandle(name)— get a file handle for an existing filegetDirectoryHandle(name, { create: true })— create or open a subdirectoryremoveEntry(name, { recursive: true })— delete a file or directoryvalues()— iterate over all entries (files and subdirectories)
const dirHandle = await window.showDirectoryPicker();
// List all files in the directory
for await (const entry of dirHandle.values()) {
if (entry.kind === "file") {
console.log("File:", entry.name);
} else {
console.log("Directory:", entry.name);
}
}
// Create a subdirectory
const subDir = await dirHandle.getDirectoryHandle("assets", { create: true });
// Get a file handle inside the directory
const fileHandle = await subDir.getFileHandle("config.json");
Working with Directories Recursively
Here is a pattern for recursively listing all files in a directory tree:
async function* walkDir(dirHandle, path = "") {
for await (const entry of dirHandle.values()) {
const entryPath = path ? `${path}/${entry.name}` : entry.name;
if (entry.kind === "directory") {
const subDir = await dirHandle.getDirectoryHandle(entry.name);
yield* walkDir(subDir, entryPath);
} else {
yield entryPath;
}
}
}
Origin Private File System
The Origin Private File System (OPFS) is a separate storage endpoint that is private to your origin — invisible to the user and not accessible through the file picker. It is optimized for performance and supports in-place writes:
const root = await navigator.storage.getDirectory();
OPFS is useful when you want persistent local storage without requiring the user to pick a directory. You get a FileSystemDirectoryHandle just like with showDirectoryPicker(), but the directory is managed entirely by the browser:
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle("cache.json", { create: true });
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify({ cached: true }));
await writable.close();
Synchronous Access in Web Workers
OPFS supports a synchronous access handle for use inside Web Workers. This is faster than the async API for frequent reads and writes, which matters when performance is critical (for example, when dealing with large files or real-time processing):
// Inside a Web Worker
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle("data.bin", { create: true });
const accessHandle = await fileHandle.createSyncAccessHandle();
// Synchronous read
const size = accessHandle.getSize();
const buffer = new DataView(new ArrayBuffer(size));
accessHandle.read(buffer, { at: 0 });
// Synchronous write
const encoder = new TextEncoder();
const encoded = encoder.encode("new content");
accessHandle.write(encoded, { at: size });
accessHandle.flush();
accessHandle.close();
createSyncAccessHandle() is only available inside a dedicated Web Worker and only works within OPFS — not with handles from file pickers.
FileSystemObserver: Watching for Changes
The FileSystemObserver lets you watch a directory for changes without polling:
const dirHandle = await navigator.storage.getDirectory();
const observer = new FileSystemObserver((records, observer) => {
for (const record of records) {
console.log("Changed:", record.changeRecord.path);
}
});
observer.observe(dirHandle);
Each FileSystemChangeRecord tells you the path that changed and what kind of change it was.
Drag and Drop Integration
You can also get file handles from drag-and-drop operations. When a user drags a file onto your page, getAsFileSystemHandle() gives you a handle (with the user’s permission):
elem.addEventListener("drop", async (e) => {
e.preventDefault();
for (const item of e.dataTransfer.items) {
if (item.kind === "file") {
const handle = await item.getAsFileSystemHandle();
if (handle.kind === "file") {
const file = await handle.getFile();
console.log("Dropped file:", file.name);
}
}
}
});
Browser Support and Security
The File System Access API is Chromium-only. It works in Chrome, Edge, and Opera, but not in Firefox or Safari as of early 2026. Always check for support before using it:
if ("showOpenFilePicker" in window) {
// Use the API
} else {
// Fall back to <input type="file">
}
The API requires a secure context (HTTPS) or localhost. Permission to access files persists across sessions until the user revokes it through the browser’s site settings.
See Also
- javascript-streams-api — the Streams API, which
Fileobjects use for reading data - javascript-indexeddb — another browser storage option, useful for structured data
- browser-storage — overview of all browser storage options including OPFS, IndexedDB, and Cache API