10LOC
Chrome APIsadvanced

The File System Access API for native file read/write

Published December 11, 2026

// TypeScript's DOM lib has FileSystemFileHandle but not window.showOpenFilePicker
// itself yet -- narrow cast for just the missing entry point.
type ShowOpenFilePicker = (opts: {
  types: { description: string; accept: Record<string, string[]> }[];
}) => Promise<FileSystemFileHandle[]>;

let fileHandle: FileSystemFileHandle | undefined;

export const openTextFile = async () => {
  const showOpenFilePicker = (window as unknown as { showOpenFilePicker: ShowOpenFilePicker }).showOpenFilePicker;
  [fileHandle] = await showOpenFilePicker({
    types: [{ description: "Text", accept: { "text/plain": [".txt"] } }],
  });
  const file = await fileHandle.getFile();
  return file.text();
};

export const saveTextFile = async (contents: string) => {
  if (!fileHandle) throw new Error("Open a file before saving");
  const writable = await fileHandle.createWritable();
  await writable.write(contents);
  await writable.close();
};

What

window.showOpenFilePicker() returns a FileSystemFileHandle for a file the user picked, and handle.createWritable() opens a stream you can write back to the same file — a real edit-and-save loop, not a download.

Why it matters

A plain <input type="file"> gives you a one-shot, read-only snapshot of a file: you can read its contents, but "saving" means generating a new Blob and triggering a download to wherever the browser's download folder is, disconnected from the original file. That's fine for a one-time import, but it can't support a "Save" button that overwrites the file the user opened — every browser-based text/image/code editor that supports real save-in-place (not "download a copy") is built on this API.

The handle you get back from showOpenFilePicker() is a durable reference: hold onto it, and createWritable() gives you a FileSystemWritableFileStream that writes straight to that path on disk, no picker prompt for the write itself (the browser may re-prompt for permission if it lapses, but not for the destination).

How it works

  • showOpenFilePicker({ types }) restricts the picker to a file type and returns an array of handles even for a single-file pick (default is multiple: false), hence the destructuring assignment.
  • fileHandle.getFile() returns a File object — the same interface <input type="file"> gives you — so .text()/.arrayBuffer() work identically.
  • createWritable() on the same handle opens a stream targeting the original file. write() accepts a string, Blob, or ArrayBuffer directly.
  • close() is what actually commits the write and releases the lock — a writable stream that's never closed leaves the file either unchanged or in an undefined partial state, so this isn't optional cleanup.
  • The guard in saveTextFile — throwing if there's no open handle — exists because calling createWritable() on undefined is a real mistake a "Save" button wired up before "Open" will make.

Gotchas

  • This is a Chromium-only API. Firefox and Safari do not implement showOpenFilePicker, showSaveFilePicker, or showDirectoryPicker in any released version, and neither engine has signaled plans to. They only support the Origin Private File System (navigator.storage.getDirectory()), a sandboxed filesystem the user never sees — a fundamentally different feature that doesn't touch real files on disk.
  • Because of that gap, this API is unsuitable as your only file I/O path for anything that needs to work outside Chromium — pair it with an <input type="file"> + download fallback, feature-detected via "showOpenFilePicker" in window.
  • TypeScript's default lib.dom.d.ts support for these types varies by TypeScript version; if FileSystemFileHandle or showOpenFilePicker aren't recognized, check your TS version before assuming a runtime problem.
  • Permission to a previously-granted handle can lapse (e.g., after the tab is reloaded); calling createWritable() on a stale handle can reject, so production code needs a queryPermission()/requestPermission() check the snippet omits for brevity.

Related

  • <input type="file"> — the universally-supported, read-only, download-to-save alternative this API is an upgrade over, on the engines that support it.
  • The Origin Private File System — Firefox and Safari's sandboxed alternative; good for app-private storage, not for editing a user's real files.