File System Access API in Practice: Reading and Writing Files Directly in the Browser
AI generated
JS
() =>
JavaScript · Browser APIs · File System
File System Access API in Practice
Reading and Writing Files Directly in the Browser

The File System Access API gives web applications real read and write access to local files and folders, without the classic detour through uploads and downloads. Anyone building editors, image tools or data exports in the browser gets behavior that feels almost like a native desktop application, including a permission model and persistent handles.

18 min read showOpenFilePicker · showSaveFilePicker · showDirectoryPicker Chrome · Edge · Opera (secure context)

1. Why the File System Access API exists

Before the File System Access API, working with local files in the browser was a compromise. An <input type="file"> delivered a one time snapshot of a file as a File object, and every change had to be handed back to the user as a fresh copy through a new download. There was no way to reopen the same file, write into it, or browse a directory in a structured way. For text editors, image editing or developer tools in the browser, that was a noticeable step behind native applications.

The File System Access API closes exactly that gap. It provides three central entry points, window.showOpenFilePicker(), window.showSaveFilePicker() and window.showDirectoryPicker(), each of which opens a native file dialog and returns a FileSystemHandle. Through this handle a file can be read, overwritten or a whole directory traversed recursively, without the user having to confirm a dialog again at every step. That is what makes browser based editors like VS Code for the Web or Photopea genuinely usable.

It is important not to confuse the File System Access API with the older, long deprecated File and Directory Entries API, which only offered read only access through webkitdirectory. The new API was deliberately created as a standard at the WHATWG and is continuously extended with capabilities such as synchronous access from web workers, which also makes it interesting for performance critical applications like WebAssembly based databases.

2. Security model: secure context, gesture and origin

Because the File System Access API grants real write access to the local file system, its permission model is stricter than most other browser APIs. Every call to showOpenFilePicker, showSaveFilePicker or showDirectoryPicker must originate from an active user gesture, a click or a key press. A call from a setTimeout or straight after the page loads is rejected by the browser with a SecurityError.

In addition, the File System Access API only works within a secure context, meaning HTTPS or localhost. Inside an <iframe> access is disabled by default unless the parent window explicitly allows it through the relevant permissions policy for file system access. This combination of user gesture, secure context and explicit permission prevents an embedded advertisement or a third party script from opening files unnoticed.

Every granted permission is also bound to a specific file or directory, not to the entire disk. A user grants access to a particular project folder, not to their whole home directory. The security model of the File System Access API therefore follows the same principle as camera or location access, granular, revocable, and always visible to the user through the address bar permissions.

3. Opening files with showOpenFilePicker

The easiest entry into the File System Access API is showOpenFilePicker(). The method is asynchronous, returns an array of FileSystemFileHandle objects, and accepts options such as types to restrict to certain file extensions and multiple for multi selection. From the handle, a current File object can be requested at any time through getFile(), which can then be read as usual with text(), arrayBuffer() or stream().

The key difference from the classic <input type="file">: the handle remains valid as long as the page stays open or as long as it has been persisted. An application can therefore reuse the same handle later to re read the file, for example after the user has changed it externally in another program. This exact behavior is what makes browser based editors practical.


// Open a text file and read its current content
async function openTextFile() {
  try {
    const [fileHandle] = await window.showOpenFilePicker({
      types: [
        {
          description: "Text files",
          accept: { "text/plain": [".txt", ".md"] },
        },
      ],
      multiple: false,
    });

    const file = await fileHandle.getFile();
    const contents = await file.text();

    console.log(`Opened ${file.name}, ${file.size} bytes`);

    // Keep the handle around so we can write back to the
    // exact same file later without asking again.
    return { fileHandle, contents };
  } catch (err) {
    if (err.name === "AbortError") {
      console.log("User cancelled the file picker");
      return null;
    }
    throw err;
  }
}

4. Writing files with showSaveFilePicker

For writing, the File System Access API provides showSaveFilePicker(). Unlike the classic <a download> pattern, the browser asks for the target location once and afterwards returns a handle that can be written to as often as needed, without a save dialog appearing every time. To actually write, you open a FileSystemWritableFileStream with createWritable(), write data into it, and close the stream with close(), only this closing step makes the changes permanently visible.

A common mistake with the File System Access API is not closing the stream, for example because an exception skips the close() call. The file then remains in an inconsistent state, usually visible as an empty temporary file. A try/finally block around the write operation ensures the stream is closed in every case, similar to how file access is handled in other languages.


// Save (or overwrite) a file through the File System Access API
async function saveTextFile(fileHandle, content) {
  // Ask for a location only if we don't already have a handle
  if (!fileHandle) {
    fileHandle = await window.showSaveFilePicker({
      suggestedName: "notes.txt",
      types: [
        { description: "Text file", accept: { "text/plain": [".txt"] } },
      ],
    });
  }

  const writable = await fileHandle.createWritable();
  try {
    await writable.write(content);
  } finally {
    // Without close(), changes never reach the actual file on disk.
    await writable.close();
  }

  return fileHandle;
}

5. Browsing directories with showDirectoryPicker

For applications that want to represent entire project structures in the browser, the File System Access API provides a third entry point with showDirectoryPicker(). The method returns a FileSystemDirectoryHandle, which can be traversed asynchronously with for await (const [name, handle] of directoryHandle.entries()). Each entry is either a FileSystemFileHandle or another FileSystemDirectoryHandle, enabling recursive browsing of entire directory trees.

This capability of the File System Access API is the foundation for browser based IDEs that open an entire project directory, index all files, and save changes directly to disk without needing a backend file system. The method getFileHandle(name, { create: true }) or getDirectoryHandle(name, { create: true }) additionally allows creating new files and subfolders within an already authorized directory, without opening a dialog again.


// Recursively list every file in a directory tree
async function listAllFiles(directoryHandle, path = "") {
  const files = [];

  for await (const [name, handle] of directoryHandle.entries()) {
    const fullPath = path ? `${path}/${name}` : name;

    if (handle.kind === "file") {
      files.push(fullPath);
    } else if (handle.kind === "directory") {
      // Recurse into subdirectories without asking the user again
      const nested = await listAllFiles(handle, fullPath);
      files.push(...nested);
    }
  }

  return files;
}

const rootHandle = await window.showDirectoryPicker();
const allFiles = await listAllFiles(rootHandle);
console.log(`Found ${allFiles.length} files`);

6. Persisting handles and renewing permissions

A handle from the File System Access API is a structured, cloneable value and can therefore be stored directly in IndexedDB. That way an application can reopen the same project folder on the next page visit without a new file dialog, as long as the user has not revoked the permission in the meantime. This is exactly where the crucial difference from a plain reference lies: the browser does not automatically persist the permission forever, it must be checked through queryPermission() and renewed if needed through requestPermission().

This renewed request in turn requires a user gesture and cannot happen silently in the background while the page loads. The usual solution: on startup the application quietly checks with queryPermission({ mode: "readwrite" }) whether the permission still holds, and only shows a "grant access again" button when the status comes back as "prompt" instead of "granted". That avoids unnecessary dialogs while also explaining to the user why an action is needed.


// Persist a handle in IndexedDB and re-request permission later
const DB_NAME = "file-handles";
const STORE_NAME = "handles";

async function saveHandle(key, handle) {
  const db = await openHandleDb();
  const tx = db.transaction(STORE_NAME, "readwrite");
  tx.objectStore(STORE_NAME).put(handle, key);
  await tx.done;
}

async function restoreHandle(key) {
  const db = await openHandleDb();
  const handle = await db.transaction(STORE_NAME).objectStore(STORE_NAME).get(key);
  if (!handle) return null;

  const status = await handle.queryPermission({ mode: "readwrite" });
  if (status === "granted") return handle;

  // Re-requesting permission always needs a user gesture (e.g. click)
  const granted = await handle.requestPermission({ mode: "readwrite" });
  return granted === "granted" ? handle : null;
}

7. Large files and streaming writes

An often overlooked advantage of the File System Access API over blob downloads shows up with large files. A classic download through URL.createObjectURL() needs the entire file content available as a blob in memory first before it can be written. The FileSystemWritableFileStream, however, is a real WritableStream that writes data to disk in chunks as it is produced, for example directly from a network response through pipeTo().

For video exports, database dumps or large CSV exports this drastically reduces memory usage, because the full content never has to sit in RAM at once. In addition, the File System Access API has supported synchronous access from web workers for some time through createSyncAccessHandle(), which is noticeably faster than the asynchronous path for file based SQLite builds like wa-sqlite, because it reads and writes directly in a blocking fashion without promise overhead.

8. Fallback strategies for Safari and Firefox

Safari and Firefox do not yet fully support the File System Access API, which is why production code always needs feature detection. The most reliable check is "showOpenFilePicker" in window, since pure user agent sniffing approaches quickly become outdated with browser updates. If the API is missing, you fall back to the classic pattern with <input type="file"> for reading and a programmatically created <a download> link for saving.

It matters that the fallback exposes the same outward interface, so that the rest of the application code does not have to branch between two APIs. A small abstraction layer that returns either a real FileSystemFileHandle or a simulated object with the same method names keeps the rest of the codebase free of conditionals. Libraries such as browser-fs-access already implement exactly this pattern, and are the more pragmatic choice for many projects than a custom implementation.


// Feature detection with a graceful fallback
async function pickAndSaveFile(content, suggestedName) {
  if ("showSaveFilePicker" in window) {
    const handle = await window.showSaveFilePicker({ suggestedName });
    const writable = await handle.createWritable();
    await writable.write(content);
    await writable.close();
    return;
  }

  // Fallback for Safari/Firefox: classic download link
  const blob = new Blob([content], { type: "text/plain" });
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = suggestedName;
  link.click();
  URL.revokeObjectURL(url);
}

9. File System Access API compared to classic patterns

The decision between the File System Access API and classic browser patterns depends heavily on the use case. For one off uploads a simple <input type="file"> is often still enough, but for repeated reading and writing of the same file the new API is clearly superior.

Requirement Classic pattern File System Access API Benefit
Reopen a file Not possible without a new upload FileSystemFileHandle Persistent handle instead of a snapshot
Write into the same file Download as new copy only createWritable() Overwrite instead of duplicate
Browse directory webkitdirectory (read only) showDirectoryPicker() Read and write, recursively
Large files Full blob in RAM Streaming writes Lower memory usage
Browser support All browsers Chromium only Fallback required

In practice, successful applications combine both approaches: the File System Access API is used where it is available, while the classic upload/download path remains as a fallback for Safari and Firefox. This dual strategy ensures the application works everywhere, while Chromium users benefit from the more convenient, more direct file management.

Mironsoft

JavaScript development, browser APIs and modern web applications

Planning an editor or file feature in the browser?

We integrate the File System Access API into your web application, including the permission model, persistence through IndexedDB and a robust fallback for Safari and Firefox.

API integration

showOpenFilePicker, showSaveFilePicker and showDirectoryPicker connected cleanly

Fallback concept

Robust feature detection and upload/download alternative for every browser

Performance tuning

Streaming writes for large files instead of a blob in memory

10. Summary

The File System Access API lifts the handling of local files in the browser to a level that feels almost like a native application. Instead of one off uploads and a new download with every change, it allows persistent FileSystemHandle objects that enable repeated reading, writing and recursive browsing of directories. The security model with user gesture, secure context and granular permissions ensures this power does not become a gateway for unnoticed file access.

Anyone using the File System Access API in production should keep three things in mind: persist handles consistently in IndexedDB, check permissions on every page visit through queryPermission() instead of assuming them blindly, and always keep a fallback strategy ready for Safari and Firefox. With this combination you can build web editors, file managers and export tools that offer the full native experience in supporting browsers and degrade reliably everywhere else.

File System Access API in Practice — Key Takeaways

Three entry points

showOpenFilePicker, showSaveFilePicker and showDirectoryPicker each return a persistent FileSystemHandle.

Permission model

Every call needs a user gesture and a secure context. Permissions are granular per file and revocable.

Persistence

Store handles in IndexedDB, check permission on the next visit with queryPermission() instead of asking again.

Fallback

Feature detection with "showOpenFilePicker" in window, keep an upload/download pattern ready for Safari and Firefox.

11. FAQ: File System Access API in Practice

1What is the File System Access API?
Real read and write access to local files through showOpenFilePicker, showSaveFilePicker and showDirectoryPicker with persistent handles.
2Which browsers support it?
Chrome, Edge, Opera fully. Safari and Firefox do not, a fallback is essential.
3Why is a user gesture required?
Prevents scripts from opening file dialogs unasked when the page loads.
4Persist a handle beyond a page visit?
Yes, in IndexedDB. Check permission again on the next visit with queryPermission.
5Stream not closed, what happens?
Changes never reach the file. Use try/finally around close().
6Safe against malicious scripts?
Yes, through secure context, user gesture and granular, revocable permissions per file.
7Browse a directory recursively?
Get showDirectoryPicker, iterate with entries(), recurse on kind directory.
8Suitable for large files?
Yes, streaming writes save memory compared to blob downloads.
9Build a fallback for Safari/Firefox?
Feature check, fall back to the classic input/download pattern if missing.
10Ready made libraries available?
browser-fs-access already encapsulates feature detection and fallback fully.