Introduction
For decades, web applications were limited to reading files through a clunky <input type="file"> element and had no reliable way to save changes back to the original file. The File System Access API changes this entirely. It enables web applications to read, write, and manage files and directories directly on the user’s local file system — with user permission, of course. This opens the door to building text editors, image editors, IDEs, and productivity tools that feel native.
Opening Files with showOpenFilePicker
The window.showOpenFilePicker() method returns a promise resolving to an array of FileSystemFileHandle objects. You can configure it with options for multiple selection, file type filtering, and MIME type restrictions:
const [fileHandle] = await window.showOpenFilePicker({
types: [{
description: 'Markdown Files',
accept: { 'text/markdown': ['.md'] }
}],
multiple: false
});
const file = await fileHandle.getFile();
const content = await file.text();
document.querySelector('#editor').value = content;
The FileSystemFileHandle interface provides two key methods: getFile() for reading and createWritable() for writing. Reading supports file.text(), file.arrayBuffer(), and file.stream() depending on the data type. Handle errors gracefully — user cancellation throws AbortError, while permission revocation throws NotAllowedError.
Saving Files with showSaveFilePicker
For saving, window.showSaveFilePicker() returns a FileSystemFileHandle pointing to the user-selected save location. Writing involves creating a writable stream, writing content, and closing the stream:
const fileHandle = await window.showSaveFilePicker({
suggestedName: 'document.md',
types: [{
description: 'Markdown Files',
accept: { 'text/markdown': ['.md'] }
}]
});
const writable = await fileHandle.createWritable();
await writable.write(editorContent);
await writable.close();
For large files, use streaming writes with the WriteParams object to write in chunks without loading the entire file into memory:
const writable = await fileHandle.createWritable();
const encoder = new TextEncoder();
const chunkSize = 1024 * 1024; // 1MB chunks
for (let offset = 0; offset < content.length; offset += chunkSize) {
const chunk = content.slice(offset, offset + chunkSize);
await writable.write({ type: 'write', data: encoder.encode(chunk), position: offset });
}
await writable.close();
The WriteParams object also supports type: 'truncate' to resize the file and type: 'seek' to reposition the write cursor, giving you precise control over file content.
Working with Directories
The window.showDirectoryPicker() method returns a FileSystemDirectoryHandle, enabling recursive traversal and modification of directory structures:
const dirHandle = await window.showDirectoryPicker();
async function traverseDirectory(dirHandle, path = '') {
for await (const [name, handle] of dirHandle.entries()) {
const fullPath = `${path}/${name}`;
if (handle.kind === 'directory') {
console.log(`📁 ${fullPath}`);
await traverseDirectory(handle, fullPath);
} else {
console.log(`📄 ${fullPath}`);
}
}
}
await traverseDirectory(dirHandle);
Creating new files and directories is straightforward:
const newFile = await dirHandle.getFileHandle('notes.md', { create: true });
const newDir = await dirHandle.getDirectoryHandle('images', { create: true });
await dirHandle.removeEntry('temp.log', { recursive: true });
Permissions Model
The File System Access API requires transient activation — the picker methods must be called in response to a user gesture like a click or keypress. Permissions on file and directory handles persist until the page is reloaded or the handle is garbage collected. For longer-lived access, you can check and request permissions explicitly:
async function requestWritePermission(fileHandle) {
const options = { mode: 'readwrite' };
if ((await fileHandle.queryPermission(options)) === 'granted') {
return true;
}
return (await fileHandle.requestPermission(options)) === 'granted';
}
File handles can be serialized and stored in IndexedDB for persistent access across sessions — this is how IDE-like applications remember previously opened projects. Note that private browsing mode clears stored handles, and cross-origin iframes cannot access the API.
Browser Support and Fallbacks
| Feature | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
| File System Access API | 86+ | 86+ | ❌ | ❌ |
| File Handling API | 102+ | 102+ | ❌ | ❌ |
| Origin Private File System | 86+ | 86+ | ❌ | ❌ |
Always use feature detection and provide fallbacks:
if ('showOpenFilePicker' in window) {
// Use File System Access API
} else {
// Fallback to <input type="file"> for reading
const input = document.createElement('input');
input.type = 'file';
input.click();
}
For saving in unsupported browsers, fall back to <a download> with a Blob URL. The File Handling API (Chrome 102+) lets you register your web app as a file handler via the Web App Manifest, enabling users to open files directly with your app from the operating system.
Advanced Use Cases
The File System Access API enables patterns previously reserved for native applications:
- IDE-like applications: Open a project directory, read and write individual files, create new files, rename and delete entries.
- Markdown editor with live preview: Open
.mdfiles, edit with real-time preview, and save back to the original file. - Image editor with Save and Save As: Use
showSaveFilePickerfor Save As andcreateWritable()for in-place Save. - Bulk file processing: Open a directory, iterate over files, apply transformations, and save results — all without downloading or uploading.
For sandboxed storage that works without user gestures, use the Origin Private File System via navigator.storage.getDirectory(). This provides a virtual file system scoped to your origin, useful for caches and offline data.
Performance Considerations
When working with large files, always prefer streaming approaches:
- Use
file.stream()for reading large files instead offile.text()orfile.arrayBuffer(). - Write in chunks (e.g., 1MB) to avoid memory pressure.
- Use the
entries()iterator for directories with thousands of files instead of collecting all entries at once. - Debounce auto-save operations — keep the writable stream open and write periodically, but close it only when done.
- The Origin Private File System (
navigator.storage.getDirectory()) offers sandboxed storage without permission prompts, ideal for app-internal data.
Conclusion
The File System Access API transforms web applications from read-only content consumers into full-featured creative tools. While currently limited to Chromium-based browsers, the capability gap it fills is enormous. Always use feature detection and provide fallbacks using traditional <input type="file"> and <a download> patterns. For productivity apps, creative tools, and developer tools that need native file capabilities, this API eliminates the need for Electron or other native wrappers.
