Your 'Save' Button Makes Copies. The File System Access API Doesn't.
The classic web 'save' — a Blob and an <a download> link — doesn't overwrite a file, it manufactures a new one every time, leaving notes.md, notes (1).md, notes (2).md scattered in Downloads. The File System Access API fixes this with real, permissioned read-write access to a file on disk. Here's the difference, and why it's Chromium-only progressive enhancement, not a drop-in replacement.

You're building a little in-browser markdown editor. "Save" is one line: turn the textarea into a Blob, wrap it in an <a download>, click it programmatically. Ship it, feels great in your own testing — you save once, done.
A real user opens notes.md, edits it, hits Save. Then edits it again, hits Save again. Then again. They go looking for their notes later and their Downloads folder has notes.md, notes (1).md, notes (2).md, notes (3).md — four files, three of them stale, and no indication which one is the one they actually want. They didn't do anything wrong. Your "Save" button was never a save button. It was a "make a new file" button that happened to reuse a familiar icon.
Here's the pattern, and it's everywhere — CodeSandbox-style playgrounds, browser-based note apps, config generators, anything that needs to hand the user a file:
It works, in the sense that a file lands on disk. But look at what it actually is: <a download> triggers the browser's download pipeline, and the download pipeline has exactly one job — write a new file, and if the name collides, rename it so it doesn't clobber anything. That collision-avoidance is a deliberate safety feature of downloads in general (you don't want a sketchy site silently overwriting your existing files), and it's exactly the behavior you don't want from a save button. There is no API call in that snippet that means "update the file the user already has open." There's no handle to the original file at all — just a one-shot blob with a suggested name.
Try it below and watch a simulated Downloads folder fill up — every click is a new file, even though it looks and feels like "saving."
Runs right in your browser — poke at it and watch the concept react live.
The gap isn't in your code, it's in the platform. A regular <input type="file"> gives you a File object — bytes, a name, a size — but reading it doesn't hand you any way to write back to that same path. Browsers spent two decades treating "the filesystem" as something a web page should never be allowed to touch, for good reason: letting arbitrary sites read or overwrite files by name would be catastrophic. So <input type="file"> and <a download> were built to avoid ever creating that connection.
The File System Access API is what closes that gap, deliberately and with permission gates at every step. window.showOpenFilePicker() doesn't just return a file's contents — it returns a FileSystemFileHandle, a persistent reference to that specific file that your page can ask to write to later:
Saving back is where the difference actually shows up. You write through the same handle you opened:
No new filename, no collision to resolve, no (1) appended anywhere. createWritable() opens a stream to the exact file the handle points at; write() sends the new bytes; close() commits them — until then the new bytes go to a temporary file (it starts empty, since keepExistingData defaults to false), and the original is only replaced when the stream closes. Same file, every time — which is the one property a save button actually needs and the download link structurally cannot provide.
That's not a toy example above — it's real. Try the second panel in the playground: pick an actual file from your machine, edit it, hit Save (Chrome asks once whether the site may save changes — that's the permission gate), then check the file on disk. It changed. Same file, same name, no sibling copies.
This is the part every "just switch to the File System Access API" take glosses over: Firefox and Safari don't implement the part that matters here — the file pickers. They do ship the handle-and-stream half (FileSystemFileHandle, getFile(), and since Safari 26 in September 2025, createWritable()), but only for the sandboxed origin private file system (navigator.storage.getDirectory()), which never touches the user's real files. The half that hands a page an actual file — showOpenFilePicker(), showSaveFilePicker(), showDirectoryPicker() — isn't "not yet, check back next release": WebKit's standards position is "oppose" on security grounds, and Mozilla's is "negative". Chrome, Edge, Opera and most other Chromium browsers support the pickers (Brave ships them switched off); that's the whole list.
Feature-detect before you touch it, every time:
Two more constraints worth knowing before you reach for it, both there on purpose:
- It needs a secure context (HTTPS or
localhost) — same rule as most powerful browser APIs. - It needs a real user gesture. You can't call
showOpenFilePicker()on page load or from a timer with no recent click; it needs transient user activation from a click or keypress (and it's refused outright inside a cross-origin iframe), the same restriction that stops a page from silently spawning a file dialog the moment you land on it.
Neither is a bug to work around — they're the reason this API was allowed to exist at all. A picker that could open without a click, or run over plain HTTP, would just be the file-access catastrophe browsers spent years avoiding, with better ergonomics.
Don't treat this as "the File System Access API is strictly better, migrate everything." The download link is still correct when:
- It's a one-off export that will never be re-saved — a generated report, a chart image, a zip.
- You need it to work in Firefox or Safari without a degraded experience — plenty of products can't accept "20% of visitors get worse behavior."
- The mental model really is "produce a new file," like exporting a CSV snapshot of current state.
The File System Access API earns its place specifically when the mental model is edit and re-save an existing file — editors, config tools, anything where "Save" should mean "update what's already there," not "hand me a fresh copy and I'll sort out the mess myself."
Back to the markdown editor. The real fix isn't switching APIs wholesale — it's keeping a handle around once you have one, and only falling back to the download link when you don't:
First save in a supporting browser prompts once, gets a handle, and keeps it. Every save after that writes to the same file with no dialog and no duplicate. Users on Firefox or Safari get the old, honest-about-its-limits download behavior — worse, but not broken, and not lied to about what "Save" means.
Think it clicked? Take the 8-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
A "Save" button built on <a download> was never overwriting anything — it was generating a new file and letting the browser's own download manager paper over the difference with a renamed copy. The File System Access API is the first time the platform gives a page an actual handle to write back through, and it comes wrapped in exactly the restrictions you'd want from something that powerful: Chromium-only pickers, secure contexts only, user-gesture only. Feature-detect it, keep the download link as your fallback, and stop calling the old behavior a save.
Have you shipped a "save" that was quietly cloning files in someone's Downloads folder? I'd bet more of us have than will admit it in the comments.
- JSON.stringify Is Quietly Deleting Your File Uploads
- The await That Silently Breaks navigator.clipboard.writeText()
- getCurrentPosition() Doesn't Just Check — It Prompts
🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
Keep reading
One post a day, in your inbox
Each one with a runnable playground and a quiz. No pitch, no digest, unsubscribe in one click.
0 comments
Sign in to join the discussion, like comments, and save articles for later.