Modern web apps increasingly feel like native apps—editing documents, exporting files, caching large assets, and even working offline. The File System API (extended in browsers via the File System Access API) is a big part of that shift: it enables reading, writing/saving, and basic file management, including directory access, when the user explicitly grants permission.
The core idea: handles
Most work in the File System API revolves around handles—objects that represent a specific file or directory on the user’s system:
- FileSystemFileHandle for files
- FileSystemDirectoryHandle for directories
- Both derive from FileSystemHandle
Rather than giving websites broad file-system access, the model is intentionally narrow: your app typically receives a handle only after the user chooses a file or folder.
How apps get access (with user intent)
Common entry points are the built-in picker methods:
- window.showOpenFilePicker() (pick a file)
- window.showSaveFilePicker() (pick a save location / file)
- window.showDirectoryPicker() (pick a folder)
Handles can also come from user-driven flows like drag and drop (DataTransferItem.getAsFileSystemHandle()), or other platform features like the File Handling API.
Writing and saving
For typical (asynchronous) file writing, the API uses FileSystemWritableFileStream, which provides a convenient way to write data such as Blobs, strings, or buffers, then close the stream to commit changes.
OPFS: private, fast, and great for offline
In addition to user-visible files and folders, the API includes the Origin Private File System (OPFS)—a storage area that is private to your site’s origin and not exposed in the user’s normal file system. OPFS is designed for performance and supports efficient workflows like:
- Offline-capable editors that update data in-place
- Large media downloads (even partial downloads) for offline viewing
- Games and apps that cache many assets locally
For high-performance scenarios (especially in dedicated Web Workers), OPFS supports FileSystemSyncAccessHandle, which enables synchronous, in-place reads/writes—useful when async overhead is too costly (for example, some WebAssembly-heavy workloads).
Security and platform notes
This API is designed with strong guardrails:
- Access to user files/directories generally requires explicit user permission
- It’s available only in secure contexts (HTTPS)
- Some parts are available in Web Workers, and OPFS sync access is worker-focused
File System API Demo
Local File Editor
Ready to open a file...
body {
background-color: #0B0F14;
/* Deep Background */
color: #E5E7EB;
/* Platinum Text */
font-family: system-ui, -apple-system, sans-serif;
display: flex;
justify-content: center;
padding-top: 50px;
margin: 0;
}
.container {
width: 800px;
max-width: 90%;
}
header {
margin-bottom: 20px;
}
h1 {
font-weight: 600;
letter-spacing: -0.5px;
margin: 0 0 5px 0;
}
.status {
color: #9CA3AF;
font-size: 0.9rem;
margin: 0;
min-height: 1.2em;
/* Prevents layout shift */
}
.toolbar {
margin-bottom: 15px;
display: flex;
gap: 10px;
}
button {
background-color: #111827;
/* Surface */
border: 1px solid #243041;
/* Border Color */
color: #E5E7EB;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
button:hover:not(:disabled) {
background-color: #243041;
border-color: #E5E7EB;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
textarea {
width: 100%;
height: 400px;
background-color: #111827;
border: 1px solid #243041;
color: #E5E7EB;
padding: 15px;
border-radius: 8px;
font-family: 'Courier New', Courier, monospace;
font-size: 1rem;
resize: vertical;
box-sizing: border-box;
}
textarea:focus {
outline: none;
border-color: #3B82F6;
/* Focus accent */
}
let fileHandle; // Variable to store the handle so we can save back to the same file later
const textEditor = document.querySelector('#textEditor');
const btnOpen = document.querySelector('#btnOpen');
const btnSave = document.querySelector('#btnSave');
const btnSaveAs = document.querySelector('#btnSaveAs');
const statusMsg = document.querySelector('#statusMsg');
/**
* 1. OPEN FILE
* Uses window.showOpenFilePicker() to prompt the user to select a file.
*/
btnOpen.addEventListener('click', async () => {
// 1. Feature Detection Check
if (!window.showOpenFilePicker) {
alert(
'Your browser does not support the File System Access API. Try Chrome, Edge, or Opera.'
);
return;
}
try {
// Options: limit selection to text files
const options = {
types: [
{
description: 'Text Files',
accept: {
'text/plain': ['.txt', '.js', '.html', '.css', '.md'],
'application/json': ['.json']
}
}
]
};
// Open the file picker (returns an array of handles)
[fileHandle] = await window.showOpenFilePicker(options);
// Get the actual File object (metadata + content) from the handle
const file = await fileHandle.getFile();
const contents = await file.text();
// Load content into UI
textEditor.value = contents;
btnSave.disabled = false; // Enable save button now that we have a handle
statusMsg.textContent = `Editing: ${file.name}`;
} catch (err) {
// Handle user cancelling the dialog
console.error('File open cancelled or failed:', err);
}
});
/**
* 2. SAVE FILE (Direct)
* Uses the existing fileHandle to write changes back to the original file.
*/
btnSave.addEventListener('click', async () => {
try {
if (!fileHandle) return; // Safety check
// Create a writable stream to the file
const writable = await fileHandle.createWritable();
// Write the contents of the textarea
await writable.write(textEditor.value);
// Close the file (saves changes)
await writable.close();
statusMsg.textContent = 'File saved successfully!';
setTimeout(
() => (statusMsg.textContent = `Editing: ${fileHandle.name}`),
2000
);
} catch (err) {
console.error('Save failed:', err);
statusMsg.textContent = 'Error saving file.';
}
});
/**
* 3. SAVE AS (New File)
* Uses showSaveFilePicker() to create a NEW file handle.
*/
btnSaveAs.addEventListener('click', async () => {
try {
const options = {
types: [
{
description: 'Text Files',
accept: { 'text/plain': ['.txt'] }
}
]
};
// Open "Save As" dialog and get a NEW handle
const newHandle = await window.showSaveFilePicker(options);
// Create writable stream
const writable = await newHandle.createWritable();
await writable.write(textEditor.value);
await writable.close();
// Update our current handle to work with the new file
fileHandle = newHandle;
btnSave.disabled = false;
statusMsg.textContent = `Saved as: ${newHandle.name}`;
} catch (err) {
console.error('Save As cancelled:', err);
}
});
When to consider it
If your app needs a smoother “desktop-like” workflow—open/edit/save, export files, manage a folder of project assets, or provide robust offline capabilities—the File System API can help bridge the gap between web and native experiences, while keeping user intent and security at the center.

