Local-First Web Apps Get Real: The Power and Friction of showDirectoryPicker
The File System Access API unlocks native-grade directory access in the browser, but fragmentation and security constraints demand a careful architectural approach.
For decades, the web sandbox has treated the local file system as hostile territory. If a web application wanted to interact with your files, it was forced to play a tedious game of pass-the-blob: users uploaded files through file inputs, edited them in memory, and downloaded the results back to their Downloads folder with appended filenames like document (3).txt.
The File System Access API, and specifically the window.showDirectoryPicker() method, fundamentally changes this dynamic. By allowing users to grant a web application read and write access to an entire local directory, the browser transitions from a sandboxed document viewer into a co-equal execution environment for local-first development tools, IDEs, and media editors.
But this capability leap comes with significant architectural trade-offs. While the API enables native-like workflows directly in the browser, developers must navigate a fragmented browser compatibility landscape, strict security constraints, and the realities of recursive asynchronous file operations.
Under the Hood: The Directory Picker Lifecycle
Unlike the deprecated sandboxed File System API or the drag-and-drop File and Directory Entries API, showDirectoryPicker() operates directly on the user's actual file system. It returns a FileSystemDirectoryHandle, which acts as the entry point for traversing, reading, and writing files within that directory tree.
To prevent malicious scripts from silently scanning a user's hard drive, the API enforces strict security guardrails. It requires a secure context (HTTPS) and can only be triggered by transient user activation—meaning it must be called directly inside an event handler for a user interaction, such as a button click.
// Feature detection
const supportsFileSystemAccess = 'showDirectoryPicker' in window;
async function getLocalDirectory() {
if (!supportsFileSystemAccess) {
throw new Error("File System Access API not supported.");
}
try {
const dirHandle = await window.showDirectoryPicker({
mode: "readwrite", // Defaults to "read"
startIn: "documents", // Suggested starting directory
id: "workspace-picker" // Browser remembers different directories for different IDs
});
return dirHandle;
} catch (err) {
if (err.name === 'AbortError') {
// User closed the picker without selecting a folder
console.warn("User aborted the directory selection.");
} else if (err.name === 'SecurityError') {
// Blocked by same-origin policy or called without user activation
console.error("Security restriction blocked the directory picker.");
} else {
console.error("Failed to open directory:", err);
}
}
}
The id option is a subtle but critical feature for developer experience. By passing a consistent ID, the browser remembers the last selected directory for that specific context, allowing users to return to their workspace on subsequent page loads without navigating the system folder tree again.
flowchart TD
A[User Click Event] --> B{Supports API?}
B -- No --> C[Fallback: input webkitdirectory]
B -- Yes --> D[window.showDirectoryPicker]
D -->|User Cancels / Sensitive Folder| E[AbortError / SecurityError]
D -->|Success| F[FileSystemDirectoryHandle]
F --> G[dirHandle.values Async Iterator]
Practical Implementation: Recursive Directory Traversal
Once you have a FileSystemDirectoryHandle, working with it requires asynchronous iteration. The handle exposes a .values() method, which returns an async iterator yielding either FileSystemFileHandle or FileSystemDirectoryHandle objects.
When building developer tools—such as a local code searcher, a markdown-based note-taking app, or a line-counter (wc) tool—you must recursively traverse this tree. However, naive recursion will quickly choke on massive directories like .git or node_modules.
Here is a production-ready implementation of a recursive directory reader that includes a filtering mechanism to skip ignored directories:
/**
* Recursively reads a directory handle and builds a tree representation.
*
* @param {FileSystemDirectoryHandle} dirHandle
* @param {Set<string>} ignoreList - Folder names to skip (e.g., 'node_modules', '.git')
* @param {string} currentPath - Cumulative path for tracking
* @returns {Promise<Object>}
*/
export async function readDirectoryRecursively(dirHandle, ignoreList = new Set(), currentPath = "") {
const result = {
name: dirHandle.name,
kind: "directory",
path: currentPath || dirHandle.name,
children: []
};
// Iterate asynchronously over directory contents
for await (const entry of dirHandle.values()) {
const entryPath = currentPath ? `${currentPath}/${entry.name}` : entry.name;
if (entry.kind === "directory") {
if (ignoreList.has(entry.name)) {
continue; // Skip ignored directories
}
const subDirectory = await readDirectoryRecursively(entry, ignoreList, entryPath);
result.children.push(subDirectory);
} else if (entry.kind === "file") {
result.children.push({
name: entry.name,
kind: "file",
path: entryPath,
handle: entry // Keep reference to write back to the file later
});
}
}
return result;
}
To read the contents of a file handle discovered during traversal, you call handle.getFile(), which returns a standard File object containing a blob. From there, you can extract the text or array buffer:
async function readFileContent(fileHandle) {
const file = await fileHandle.getFile();
return await file.text();
}
The Developer's Dilemma: Security, Friction, and Browser Politics
While the technical implementation is straightforward, the architectural decisions surrounding the File System Access API are highly complex. The API is currently a battleground of differing browser philosophies.
1. The Compatibility Gap
Currently, the File System Access API is supported primarily on Chromium-based browsers (Chrome, Edge, Opera) on Windows, macOS, ChromeOS, Linux, and Android. Brave supports it, but keeps it hidden behind a flag.
Firefox and Safari remain notable holdouts. Their resistance is rooted in privacy and security concerns: granting a web application access to a local directory exposes a massive attack surface. If a malicious site tricks a user into selecting their home directory, the site could theoretically read sensitive SSH keys, browser profiles, or personal documents.
Because of this fragmentation, you cannot treat showDirectoryPicker() as a baseline dependency. If you are building a consumer-facing web application, you must implement a fallback strategy. This typically means falling back to the non-standard <input type="file" webkitdirectory> element, which allows users to upload a directory but does not support writing changes back to disk, or using the Origin Private File System (OPFS) for sandboxed, local-only storage.
2. The Permission Prompt Friction
Even on supported browsers, the user experience is not entirely seamless. To write to a file, the browser will prompt the user with an explicit permission dialog. This prompt must be answered every time the origin is loaded fresh, meaning your application cannot silently persist write permissions across browser sessions without user consent.
Furthermore, the browser's user agent will actively block access to sensitive system folders (such as the Windows directory, macOS Library, or user root directories) to prevent catastrophic overrides.
The Verdict: Genuine Shift or Niche Capability?
Is showDirectoryPicker() a gimmick, or is it ready for production?
The answer depends entirely on your target audience and application architecture. For general-purpose consumer web apps, the lack of Firefox and Safari support makes it a progressive enhancement at best. You cannot build a mainstream SaaS product that relies solely on this API for its core loop.
However, for developer tools, internal enterprise utilities, and specialized creative suites (such as WebGPU-powered video editors or local-first Markdown editors), this API is a massive paradigm shift. It allows developers to build highly performant, local-first applications that run entirely in the browser while respecting the user's ownership of their data.
By bypassing the cloud, you eliminate server-side storage costs, reduce latency to zero, and provide an offline-first experience that feels indistinguishable from a native desktop application. If your user base is technical enough to use Chromium-based browsers, or if you are packaging your web app via Electron or Tauri, showDirectoryPicker() is ready to be the backbone of your local file-handling architecture.
Sources & further reading
- window.showDirectoryPicker opens up a whole new world — steveharrison.dev
- Window: showDirectoryPicker() method - Web APIs | MDN — developer.mozilla.org
- The File System Access API: simplifying access to local files | Capabilities | Chrome for Developers — developer.chrome.com
- hckr news - Hacker News sorted by time — hckrnews.com
- How I implemented wc in the browser in 3 days — blog.kowalczyk.info
Rachel has been embedded in the developer tooling ecosystem for nearly eight years, covering everything from IDE wars and package-manager drama to the quiet rise of AI-assisted coding. She has a soft spot for open-source maintainers and an unhealthy number of terminal emulators installed on a single laptop.
Discussion 1
i'm reminded of the old netscape filesystem api from the 90s, we had similar issues with security and fragmentation back then, nice to see we're revisiting this problem with a more modern approach