The File System Access API is a web API that allows read and write access to a user’s local files. It unlocks new capabilities to build powerful web applications, such as text editors or IDEs, image editing tools, improved import/export, all in the frontend. Let’s look into how to get started using this API.

Reading files with the File System Access API
Before diving into the code required to read a file from the user’s system, an important detail to keep in mind is that calling the File System Access API needs to be done by a user gesture, in a secure context. In the following example, we’ll use a click event.
Reading from a single file
Reading data from a file can be done in less than 10 lines of code. Here’s an example code sample:
let fileHandle;
document.querySelector(".pick-file").onclick = async () => {
[fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
const content = await file.text();
return content;
};
Let’s imagine we have a button in our HTML with the class .pick-file
. When clicking on this button, we launch the file picker by calling window.showOpenFilePicker()
, and we store the result from this query in a variable called fileHandle
.
What we get back from calling showOpenFilePicker()
is an array of FileSystemFileHandle
objects representing each file we selected. As this example is for a single file, we destructure the result. I’ll show how to select multiple files a bit later.
These objects contain a kind
and name
property. If you were to use console.log(fileHandle)
, you would see the following object:
FileSystemFileHandle {kind: 'file', name: 'data.txt'}
The kind
can either be file
or directory
.
On fileHandle
, we can then call the getFile()
method to get details about our file. Calling this method returns an object with a few properties, including a timestamp of when the file was last modified, the name of the file, its size, and type.
Finally, we can call text()
on the file to get its content.
Reading from multiple files
To read from multiple files, we need to pass an options
object to showOpenFilePicker()
.
For example:
let fileHandles;
const options = {
multiple: true,
};
document.querySelector(".pick-file").onclick = async () => {
fileHandles = await window.showOpenFilePicker(options);
// The rest of the code will be shown below
};
By default, the multiple
property is set to false
. Other options can be used to indicate the types of files that can be selected.
For example, if we only wanted to accept .jpeg
files, the options object would include the following:
const options = {
types: [
{
description: "Images",
accept: {
"image/jpeg": ".jpeg",
},
},
],
excludeAcceptAllOption: true,
};
In this example, fileHandles
is an array containing multiple files, so getting their content would be done in the following way:
let fileHandles;
const options = {
multiple: true,
};
document.querySelector(".pick-file").onclick = async () => {
fileHandles = await window.showOpenFilePicker(options);
const allContent = await Promise.all(
fileHandles.map(async (fileHandle) => {
const file = await fileHandle.getFile();
const content = await file.text();
return content;
})
);
console.log(allContent);
};
Writing to a file with the File System Access API
The File System Access API also allows you to write content to files. First, let’s look into how to save a new file.
Writing to a new file
Writing to a new file can also be done in a very short amount of code!
document.querySelector(".save-file").onclick = async () => {
const options = {
types: [
{
description: "Test files",
accept: {
"text/plain": [".txt"],
},
},
],
};
const handle = await window.showSaveFilePicker(options);
const writable = await handle.createWritable();
await writable.write("Hello World");
await writable.close();
return handle;
};
If we imagine a second button with the class save-file
, on click, we open the file picker with the method showSaveFilePicker()
and we pass in an option
object containing the type of file to be saved, here a .txt
file.
Calling this method will also return a FileSystemFileHandle
object like in the first section. On this object, we can call the createWritable()
method that will return a FileSystemWritableFileStream
object. We can then write some content to this stream with the write()
method in which we need to pass the content.
Finally, we need to call the close()
method to close the file and finish writing the content to disk.
If you wanted to write some HTML code to a file for example, you would only need to change what’s in the options
object to accept "text/html": [".html"]
and pass some HTML content to the write()
method.
Editing an existing file
If you’d like to import a file and edit it with the File System Access API, an example code sample would look like:
let fileHandle;
document.querySelector(".pick-file").onclick = async () => {
[fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
const writable = await fileHandle.createWritable();
await writable.write("This is a new line");
await writable.close();
};
If you’ve been following the rest of this post, you might recognize that we start with the showOpenFilePicker()
and getFile()
methods to read a file and we then use createWritable()
, write()
and close()
to write to that same file.
If the file you’re importing already has content, this code sample will replace the current content with the new one passed into the write()
method.
Additional File System Access API features
Without going into too much detail, the File System Access API also lets you list files in directories and delete files or directories.
Read directories
Reading directories can be done with a tiny bit of code:
document.querySelector(".read-dir").onclick = async () => {
const directoryHandle = await window.showDirectoryPicker();
for await (const entry of directoryHandle.values()) {
console.log(entry.kind, entry.name);
}
};
If we add a new button with the class .read-dir
, on click, calling the showDirectoryPicker()
method will open the file picker and, when selecting a directory on your computer, this code will list the files found in that directory.
Delete files
Deleting a file in a directory can be done with the following code sample:
document.querySelector(".pick-file").onclick = async () => {
const [fileHandle] = await window.showOpenFilePicker();
await fileHandle.remove();
};
If you want to delete a folder, you only need to make a small change to the code sample above:
document.querySelector(".read-dir").onclick = async () => {
const directoryHandle = await window.showDirectoryPicker();
await directoryHandle.remove();
};
Finally, if you want to remove a specific file when selecting a folder, you could write it like this:
// Delete a single file named data.txt in the selected folder
document.querySelector(".pick-folder").onclick = async () => {
const directoryHandle = await window.showDirectoryPicker();
await directoryHandle.removeEntry("data.txt");
};
And if you want to remove an entire folder, you would need the following lines:
// Recursively delete the folder named "data"
document.querySelector(".pick-folder").onclick = async () => {
const directoryHandle = await window.showDirectoryPicker();
await directoryHandle.removeEntry('data', { recursive: true });
};
File System Access API browser support
At the moment, IE and Firefox don’t seem to be supporting the File System Access API. However, there exists a ponyfill called browser-fs-access.
This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.
Desktop
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
116 | 115 | No | 113 | TP |
Mobile / Tablet
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
113 | No | 113 | 16.5 |
Wrapping up
If you’d like to try the File System Access API, check out this live demo text editor built by Google engineers. Otherwise, if you’d like to learn more about this API and all its features, here are some resources:
- File System Access API (W3C Specification)
- File System Access API (MDN)
- Contrast Ratio Range, replaceAll Method, Native File System API (Šime Vidas)
- The File System Access API: simplifying access to local files (web.dev)
- Reading and writing files and directories with the browser-fs-access library (web.dev)
- browser-fs-access repo (GitHub)
That is very cool. I look forward to watching this mature!
What would be considered a valid “user gesture”? I guess it has to be in an event handler of a certaim events (is there a list?), but is it only that? Does it have to happen right away in the handler? Must be synchronously?
While I like this API – I even use it in this project – I don’t like how unstable it is: the
.delete()
method, as an example, is quite new and is not supported in many Chromium forks and derived browsers (like Ungoogled Chromium). The most supported way to deleting files if you want to support browsers other than official Chrome and Chromium, at this moment, is using.removeEntry()
.The same thing applies to
.move()
: even if it’s quite a useful feature it was added way later in the spec and most pages about this API don’t have any note about it not being well supported. Also for a while there was.rename()
but.move()
now does the same thing and.rename()
was removed.Because how unstable it is is no surprise only Chromium implements it: potential security issues aside, how can other browsers keep up with those changes even after it got in Chrome/Chromium stable?
I put security issues aside because what are other browsers to improve security? Safari don’t have auto-update, Firefox removed the prompt to confirm downloads in recent versions (which even used to have a delay for security purposes!).
For me, while this API makes some security issues more easier (as an example there was a way to make users download .lnk files with could link to malware without them knowing those files are dangerous making them look like image files) with the time those issues will be solved and the benefits will overcome the little insignificant potential drawbacks.
In the other hand why they can’t make this API stable before shipping? Why they make a
.rename()
method and ship it in Chrome only to remove it in the next release? Why they added.delete()
just recently? Why repeating the same issues as DOM had? Sure that doingel.remove()
is easier to doel.parentElement.removeChild(el)
, then.delete()
should had come earlier instead of repeating the same with the File System API by making.removeEntry()
work like.removeChild()
. Maybe, because the potential security issues, only Chromium contributors wanted to help writing this API and it got messed, I don’t know. Well, it’s controversial even in Chromium community: Brave removed it.By the way, other issues I found: unlike
<a download>
you can’t make it download files to a download folder without prompting the user, unlike<a download>
; it don’t sanitize file names automatically, instead if you use a invalid name it will throw an obscure error (and using npm libraries for that don’t work as they – I tried most of them – don’t sanitize all character Chromium thinks are insecure, like, for some weird reason,~
), the error, of course, don’t specify which characters are not allowed so you need to guess; and, finally, it don’t have file system limitations in file paths well, if you try to write a file which will result in a file path larger than the underlining file system accepts it will reject with a obscure error which, until I guessed what was causing it, caused some headaches.In the other hand, as you can see from my project I linked above, it have it’s own uses, is quite powerful, useful and is a great API. The fact it supports streaming file reading and writing without requiring a Service Worker hack is great: without it my project would require putting files in memory making the project unusable for handling huge files such as videos. I’m sure it will be a great and popular API in the future when it gets more stable.
I think these simplified code samples do readers a disservice. It promotes code that doesn’t check for errors or null return values. The error handling is an important part of using the API. It should at least be mentioned.
Didn’t know you needed a ssl to run code
How does this work with package files (e.g. textbundle files) or compressed archives (e.g. .zip archives)?