File Dialogs
How to open native file picker dialogs in a Tauri v2 app using React, covering single-file selection, multiple files, file filters, and default directories
A file dialog is a native window the operating system provides. The user browses the file system, picks one or more items, and hands them over to your application. For anything that involves loading a document, importing a photo, or selecting an attachment, this is the mechanism that respects user choice and system security. In Tauri v2, the dialog plugin wraps each platform’s native picker so you can trigger it from your React frontend with a single function call.
The plugin does the heavy lifting: on Windows it uses the Win32 common item dialog, on macOS it goes through the native NSOpenPanel, and on Linux it leans on GTK or XDG Desktop Portals. You write the same JavaScript, and users get the dialog that looks and behaves exactly like every other app on their machine.
Prerequisites:
This page assumes the dialog plugin is already installed and initialized in your project. If you haven’t done that yet, refer to the Dialog API Introduction. The npm package is @tauri-apps/plugin-dialog, and the Rust plugin must be registered in lib.rs.
How a file dialog reaches your code
When the user clicks a button in your React app, you call an async function that asks Tauri to spawn the native picker. The frontend thread is not frozen — your UI remains responsive while the dialog is open. Once the user confirms or cancels, the function resolves with either a file path (a string) or null. On desktop platforms the path looks like /home/user/document.pdf or C:\Users\.... On Android you get a content URI (content://...), and on iOS a similar URI scheme.
The permission system sits between your JavaScript and the dialog. Without the right permission token, the call fails silently. That’s why the very first step is making sure your capability file allows the dialog:allow-open permission.
Adding the file dialog permission
Every Tauri v2 project has a capability file — usually src-tauri/capabilities/default.json — that lists which APIs the frontend can invoke. File dialogs need the dialog:allow-open permission. You can add it manually.
Locate the default capability file
Open src-tauri/capabilities/default.json. A fresh Tauri v2 template already has a permissions array inside the main capability object.
Add the open permission
Include "dialog:allow-open" in the permissions list. Your file should contain something like this:
{
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:allow-open"
]
}
Missing permission means silent failure:
If you forget this step, calling open() from JavaScript will throw an error or return null with no visible native dialog. The browser console will show a permission-related message. Always check the capability file first when a dialog doesn’t appear.
If your app also needs a save dialog, add dialog:allow-save as well. The permissions are granular — an app that only opens files does not automatically get save privileges.
Opening a single file
The open function from @tauri-apps/plugin-dialog is the entry point. When called with default options, it presents the user with a standard file picker that returns one path or null.
Here’s a minimal React component that opens a file and displays the chosen path:
import { useState } from "react";
import { open } from "@tauri-apps/plugin-dialog";
function App() {
const [selectedFile, setSelectedFile] = useState<string | null>(null);
const pickFile = async () => {
const filePath = await open({
multiple: false,
directory: false,
});
if (filePath) {
setSelectedFile(filePath);
}
};
return (
<div>
<button onClick={pickFile}>Select a file</button>
{selectedFile && <p>Selected: {selectedFile}</p>}
</div>
);
}
export default App;
A few things happen in that snippet. The open function receives an options object. multiple: false means only one file can be picked — the dialog’s interface reflects that by using single‑click selection and disabling multi‑select controls. directory: false tells Tauri this is a file picker, not a folder picker. If you set directory: true, you get the folder‑selection dialog covered in
The function returns null when the user cancels or closes the dialog without choosing anything. That’s why we check if (filePath) before updating state. A null result is not an error — it’s a deliberate user action.
directory: true changes the dialog type:
The directory option is not a “pick a file from a specific folder” setting. It toggles between a file‑selection dialog and a folder‑selection dialog. When it’s true, the user sees a folder chooser with no file list. Confusing the two leads to “my filter isn’t working” or “I can’t see files” confusion. Folder‑specific dialogs are covered in
Picking multiple files at once
Some workflows need several files at once — importing a batch of photos, attaching multiple documents, or loading a set of configuration files. Set multiple: true and the dialog allows selecting more than one item. The return type changes from string | null to string[] | null.
import { useState } from "react";
import { open } from "@tauri-apps/plugin-dialog";
function MultiFilePicker() {
const [files, setFiles] = useState<string[] | null>(null);
const pickFiles = async () => {
const selected = await open({
multiple: true,
directory: false,
title: "Pick your images",
});
if (selected) {
setFiles(selected);
}
};
return (
<div>
<button onClick={pickFiles}>Select multiple files</button>
{files && (
<ul>
{files.map((f) => (
<li key={f}>{f}</li>
))}
</ul>
)}
</div>
);
}
export default MultiFilePicker;
The title option sets the window caption — in this example the dialog’s title bar reads “Pick your images”. The selection order depends on the platform, but the array always contains the paths the user confirmed. Tapping Escape or clicking Cancel still gives null, not an empty array.
Check your capability:
After adding the permission and launching the app, click the button. If the native dialog opens and the selected path is printed, your setup is correct. The permission model is working as intended.
Filtering visible files with extensions
A raw file picker showing every single file on disk is rarely helpful. Most applications narrow the view by file type: a text editor shows .txt and .md, an image viewer shows .png and .jpg. The filters option does exactly that. It’s an array of filter objects, each with a name (shown in the dropdown) and an extensions array.
const filePath = await open({
multiple: false,
filters: [
{ name: "Text Documents", extensions: ["txt", "md"] },
{ name: "All Files", extensions: ["*"] },
],
});
A filter does not block other files — the user can switch to “All Files” or another filter you provide and pick whatever they want. It’s a convenience, not a validation rule. After the dialog returns, you should still check the file extension in your own code if certain formats are required.
Do not include the dot in extension strings:
Extensions must be written without a leading dot. Use "png", not ".png". Adding a dot causes the filter to match nothing, leaving the user staring at an empty file list.
The * wildcard means “show everything”. You can combine multiple extensions in one filter by separating them with commas in the extensions array: ["png", "jpg", "jpeg"]. On desktop, the dialog dropdown shows the filter name, and the file list updates immediately. On mobile, the system picker applies MIME-type filtering based on the same extensions.
Starting in a specific directory
When the dialog opens, the user sees whichever directory the OS remembers from a previous pick — often the last folder they visited. To guide them to a predictable location, pass a defaultPath.
import { documentDir } from "@tauri-apps/api/path";
const startingDir = await documentDir();
const filePath = await open({
multiple: false,
defaultPath: startingDir,
});
The documentDir() function comes from the path plugin (@tauri-apps/api/path) and resolves to the user’s Documents folder on each OS. You can use any directory string — a hard‑coded path, a value from another system directory resolver, or a saved user preference.
If the path doesn’t exist, the dialog falls back to a sensible default (usually the user’s home directory or the current working directory). No error is thrown.
Platform‑specific quirks:
On macOS, the defaultPath sets the initial directory but also pre‑selects the file if the path points to an existing file. On Linux (GTK), the dialog may ignore the directory if the last‑used folder is recorded in the GTK bookmarks. These are cosmetic differences — the returned path is always correct.
Working with the returned path
On Windows, macOS, and Linux, the returned string is an absolute filesystem path like /home/me/report.pdf or C:\Users\Me\Documents\report.pdf. You can pass it to the file system plugin to read or write, or send it to a Rust command via invoke. On Android the value is a content URI (content://com.android.providers...), and on iOS it’s a file:// URI. Direct file system access through the path alone won’t work on mobile — you need the tauri-plugin-fs APIs to read the content.
import { readTextFile } from "@tauri-apps/plugin-fs";
const filePath = await open({ multiple: false });
if (filePath) {
const contents = await readTextFile(filePath);
console.log(contents);
}
This snippet works across platforms because readTextFile understands both filesystem paths and content URIs. Keeping file reading separate from dialog opening also makes the code clearer — each function does one thing.
Common mistakes and how to avoid them
Forgetting the permission. The most frequent problem: the dialog never appears, and the console shows a permission error. Double‑check that dialog:allow-open is inside the capability file’s permissions array.
Using blocking_pick_file from the frontend. The Rust API has a blocking variant intended for background threads. If you write a Tauri command that calls blocking_pick_file on the main thread, the entire app freezes until the dialog closes. The JavaScript open() is always async — use it from the frontend, and if you need the Rust side, prefer the non‑blocking pick_file method with a callback.
Ignoring the null case. User cancellation is a valid outcome. Code that assumes a string and crashes on null is a real‑world bug. Always check for null before using the result.
Mixing up directory: true and folder selection. Setting directory: true produces a folder picker, not a file picker with a folder‑style view. That option belongs to If you want to pick files starting in a certain folder, use defaultPath.
Using extensions with a dot. extensions: [".png"] silently hides all files. Remove the dot.
Summary
File dialogs in Tauri v2 give you the native OS picker with a straightforward async API. The dialog plugin manages the platform differences; your job is to call open, pass the right options, and respect the user’s choice — including cancellation. Permissions keep the frontend from silently accessing files without explicit user interaction, and the returned path integrates cleanly with the file system plugin for reading or processing.
You’ve seen how to open a single file, how to switch to multi‑file mode, and how to steer the dialog with filters and a starting directory.