Save Dialogs

Learn how to open native save dialogs in Tauri v2 to let users choose where to save files, with default filenames, file extension filters, and permission configuration.

A save dialog is the operating system's built-in window that asks the user where they want to save a file. Instead of forcing users to type a path into your app, you present the same familiar file picker they use in every other desktop program. The dialog returns a file path you can then use to write the actual content.

The Tauri v2 dialog plugin exposes this through the save function. You call it from your frontend JavaScript and it opens the native dialog, pauses your request until the user makes a choice, then gives you either the chosen path or null if the user cancelled.

The dialog does not save anything:

Calling save() only returns a file path. It does not create a file, write any data, or touch the disk. Your code must take that path and perform the actual file write using the file system API, a Rust command, or whatever library you prefer.

Setting Up the Dialog Plugin

Before any dialog code runs, the plugin must be installed on both the Rust side and the JavaScript side, and your app must grant the save dialog permission.

1

Step 1: Add the plugin to your project

Run this from your project root. It updates Cargo.toml with tauri-plugin-dialog and adds the npm package automatically.

npm run tauri add dialog

If you prefer to do the Rust and JavaScript sides separately, you can run:

cargo add tauri-plugin-dialog   # inside src-tauri
npm install @tauri-apps/plugin-dialog
2

Step 2: Initialize the plugin in Rust

In src-tauri/src/lib.rs, call .plugin(tauri_plugin_dialog::init()) on the Tauri builder.

src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
3

Step 3: Grant the save dialog permission

Tauri v2 requires explicit permission for each dialog type. Open your app’s capability file (usually src-tauri/capabilities/default.json) and add "dialog:allow-save" to the permissions array.

src-tauri/capabilities/default.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "dialog:allow-save"
  ]
}

Missing permission causes a runtime error:

If you skip this step, the save() call will fail with a permission denied error. The dialog will never appear.

Opening a Save Dialog

The save function comes from @tauri-apps/plugin-dialog. It is asynchronous and returns a promise that resolves to either a string (the chosen path) or null (the user closed the dialog or clicked Cancel).

The simplest call looks like this:

src/App.tsx
import { useState } from 'react';
import { save } from '@tauri-apps/plugin-dialog';
function App() {
  const [savedPath, setSavedPath] = useState<string | null>(null);
  const handleSave = async () => {
    const filePath = await save();
    if (filePath) {
      setSavedPath(filePath);
      console.log('Save location:', filePath);
    } else {
      console.log('Save dialog was cancelled');
    }
  };
  return (
    <div>
      <button onClick={handleSave}>Save File</button>
      {savedPath && <p>File will be saved to: {savedPath}</p>}
    </div>
  );
}
export default App;

When you click the button, the operating system’s native save dialog appears. The user navigates to a folder, types a filename, and confirms. The full absolute path (e.g., /home/user/Documents/report.txt on Linux or C:\Users\user\Documents\report.txt on Windows) lands in the filePath variable. If the user hits Cancel, filePath is null.

Always check for null:

Skipping the if (filePath) check is the most common mistake. If you try to pass null to a file writing function, your app will crash. Treat cancellation as a normal, expected outcome.

Setting a Default Filename

Users appreciate a sensible starting point. The defaultPath option lets you suggest a filename, with or without a directory.

const filePath = await save({
  defaultPath: 'untitled_document.txt',
});

If you provide only a filename, the dialog typically opens in the user’s last-used or default save directory (often Documents or Downloads). You can also supply a full path to suggest both the folder and the filename:

const filePath = await save({
  defaultPath: '/home/user/projects/export.json',
});

The user can always change both the location and the name. The defaultPath is just a suggestion.

Platform differences:

On Linux, when using portal file choosers, the defaultPath may be ignored unless the portal backend is version 4 or higher. The dialog will still appear, but it might start in the user’s default directory instead of the one you specified.

Filtering by File Extension

The filters option restricts which file types the user sees in the dialog and can automatically append the correct extension. Each filter has a human-readable name and an extensions array of bare extensions (no dots, no wildcards except "*" for all files).

const filePath = await save({
  filters: [
    { name: 'PNG Images', extensions: ['png'] },
    { name: 'JPEG Images', extensions: ['jpg', 'jpeg'] },
    { name: 'All Files', extensions: ['*'] },
  ],
});

If the user types a filename without an extension and has a filter selected, some platforms will append the first extension from that filter automatically. This behavior is platform-dependent, so never assume the extension will always be added—validate the path afterward if your app requires a specific extension.

The extensions list must not contain dots or the *. prefix. 'png' is correct; '.png' or '*.png' will not work.

Putting It Together: A Complete Save Flow

The save dialog only returns a path. To actually write a file, you need the file system plugin (@tauri-apps/plugin-fs) and its writeTextFile (or writeBinaryFile) function. This example shows a minimal end-to-end flow.

First, add the file system plugin and its write permission. Install the npm package and add "fs:allow-write" to your capability permissions array, just like you did for the dialog permission.

src/SaveButton.tsx
import { save } from '@tauri-apps/plugin-dialog';
import { writeTextFile } from '@tauri-apps/plugin-fs';
function SaveButton() {
  const handleSave = async () => {
    const filePath = await save({
      title: 'Save Your Notes',
      defaultPath: 'notes.txt',
      filters: [
        { name: 'Text Files', extensions: ['txt'] },
        { name: 'All Files', extensions: ['*'] },
      ],
    });
    if (filePath) {
      try {
        await writeTextFile(filePath, 'These are my saved notes.');
        console.log('File written successfully to', filePath);
      } catch (err) {
        console.error('Failed to write file:', err);
      }
    }
  };
  return <button onClick={handleSave}>Save Notes</button>;
}
export default SaveButton;

The flow is: open dialog → get path → write content. If the user cancels, nothing happens. If writing fails (e.g., no disk space), the error is logged. This is the standard pattern you will use for any save operation in a Tauri app.

Works when you see the path logged:

After clicking Save and confirming the dialog, your console should show the path and the file appears on disk with the content you wrote. That confirms the dialog permission, file system permission, and the write logic are all correctly wired.

How the Dialog Permission Works

The permission dialog:allow-save is a command-level gate. The dialog plugin’s default permission set already includes it if you use the auto-generated capabilities, but explicitly listing the permission in your own capability file makes the requirement visible and prevents breakage if the defaults change.

When save() is called from JavaScript, Tauri checks the capabilities attached to the window. If no capability grants dialog:allow-save, the call is rejected with a descriptive error. This is the same mechanism used for dialog:allow-open (file open dialogs) and dialog:allow-message (message dialogs).

You can also grant all dialog types at once by using the plugin’s own permission set identifier, though explicit individual permissions give you finer control.

Common Mistakes and Misconceptions

Assuming the dialog creates the file. The save dialog is a path picker, not a file writer. Your code is always responsible for creating and writing the file after the dialog closes.

Not handling cancellation. A null result is not an error; it means the user intentionally closed the dialog. Treat it as a no-op. Passing null to a file write function will throw an error.

Using dotted extensions in filters. Always use 'txt', never '.txt' or '*.txt'. Incorrect extensions cause the filter to silently fail—the dialog shows all files instead of narrowing the list.

Forgetting to add the dialog:allow-save permission. This is the most common reason a save dialog fails to open. The browser console will show a permission error, and the dialog will never appear.

Relying on automatic extension appending. Some platforms add the selected filter’s extension if the user doesn’t type one; others do not. If your app requires a specific extension, check the returned path and append the extension yourself if it’s missing.

Summary

Save dialogs give your users a native, familiar way to choose where their data goes. You set a default filename and filter the visible file types, then handle the returned path in your own code. The dialog itself never touches the disk—it only provides a path.

The pattern is always the same: open the dialog, check for cancellation, and if you got a path, write the data. Combined with the file open dialog and folder dialog, save dialogs complete the set of native file system pickers available in Tauri v2.