Accessing Local Files in the Browser With the File System Access API

The File System Access API brings read and write access to a user's local files directly from the browser. This opens the door for frontend-only applications like text editors, IDEs, and image tools that can work with files seamlessly. Like other powerful browser APIs, every call must be triggered by a user gesture and run in a secure context — a click event works perfectly.

Reading Files

Reading a single file requires remarkably little code. When a user clicks a button, call window.showOpenFilePicker() and store the result in a variable. This returns an array of FileSystemFileHandle objects — destructure it to get the single file you need.

Each FileSystemFileHandle exposes a kind property (either file or directory) and a name property. With the handle in hand, you can call getFile() to pull metadata such as file size, type, name, and last modified timestamp. To actually read the content, call text() on the returned file object.

const pickFileButton = document.querySelector(".pick-file");
pickFileButton.addEventListener("click", async () => {
  const [fileHandle] = await window.showOpenFilePicker();
  const file = await fileHandle.getFile();
  const content = await file.text();
});

For multiple files, pass an options object to showOpenFilePicker() with the multiple property set to true (it defaults to false). You can also constrain the selection to specific file types:

const pickFileButton = document.querySelector(".pick-file");
pickFileButton.addEventListener("click", async () => {
  const fileHandles = await window.showOpenFilePicker({
    multiple: true,
    types: [{
      description: "JPEG images",
      accept: { "image/jpeg": [".jpeg"] }
    }]
  });

  for (const fileHandle of fileHandles) {
    const file = await fileHandle.getFile();
    const content = await file.text();
  }
});

Writing and Saving Files

Creating a new file is as straightforward as reading one. Call showSaveFilePicker() with an options object that specifies the file type — for example, a .txt text file. This also returns a FileSystemFileHandle. From there, call createWritable() to obtain a FileSystemWritableFileStream, pass the content to its write() method, and finish by calling close() to flush data to disk.

const saveFileButton = document.querySelector(".save-file");
saveFileButton.addEventListener("click", async () => {
  const fileHandle = await window.showSaveFilePicker({
    types: [{ description: "Text files", accept: { "text/plain": [".txt"] } }]
  });
  const writable = await fileHandle.createWritable();
  await writable.write("Your file content here");
  await writable.close();
});

Editing an existing file follows the same pattern: use showOpenFilePicker() and getFile() to read, then createWritable(), write(), and close() to overwrite the content. Note that this approach replaces the entire existing content with what you pass to write().

Directories and Deletion

The API also handles directory operations, including listing contents and deleting entries. To read a directory, call showDirectoryPicker() and iterate over the entries() of the returned handle.

const readDirButton = document.querySelector(".read-dir");
readDirButton.addEventListener("click", async () => {
  const dirHandle = await window.showDirectoryPicker();
  for await (const entry of dirHandle.values()) {
    console.log(entry.getFile());
  }
});

Removing a file within a directory requires obtaining a handle to the directory first, then calling removeEntry() with the file name. To remove an entire folder, pass the recursive: true option:

const deleteFileButton = document.querySelector(".remove-file");
deleteFileButton.addEventListener("click", async () => {
  const dirHandle = await window.showDirectoryPicker();
  await dirHandle.removeEntry("todo.txt");
});

const deleteFolderButton = document.querySelector(".remove-folder");
deleteFolderButton.addEventListener("click", async () => {
  const dirHandle = await window.showDirectoryPicker();
  await dirHandle.removeEntry("todos", { recursive: true });
});

Browser Support and Resources

Internet Explorer and Firefox currently lack support in this API per Caniuse data. As a workaround, the browser-fs-access ponyfill from GoogleChromeLabs provides a fallback that uses standard file input and download elements.

For a working example, Google engineers have published a live text editor demo. For deeper reading, consult the W3C specification, the MDN documentation, or the browser-fs-access repository on GitHub.