Message Dialogs

Learn how to show native alert, confirmation, and warning message dialogs in Tauri v2 using the dialog plugin

A message dialog is a small system-level popup that interrupts the user with a short piece of text and a set of buttons. You use it when the app needs to tell the user something critical, warn them before a destructive action, or ask a yes/no question that must be answered before anything else happens. In Tauri v2, these dialogs are provided by the dialog plugin and look identical to the native dialogs on Windows, macOS, and Linux.

The plugin gives you three core functions:

  • Alert – Show a message with an “OK” button. Use it for errors, success notices, or general information that just needs to be acknowledged.
  • Confirm – Show a message with “OK” and “Cancel” buttons. Use it when the user needs to approve or reject an action that is about to happen.
  • Ask – Show a message with “Yes” and “No” buttons. Use it when a decision has two logical outcomes that are both valid next steps.

All three can be styled with a visual kind (info, warning, error) so the user instantly understands the gravity of the message without reading a word.

Quick Setup Check

If you already have the dialog plugin installed and a capability that grants message dialog permissions, you can jump straight to the usage sections. If not, follow these steps once — or see the Dialog API Introduction.

1

Add the Rust plugin

From the src-tauri directory, add the plugin crate:

cargo add tauri-plugin-dialog

Then initialize it in 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");
}
2

Install the JavaScript package

Add the npm package to your frontend:

npm install @tauri-apps/plugin-dialog
3

Grant message dialog permissions

All message dialog functions (message, ask, confirm) require the dialog:allow-message permission. Create or update a capability file in src-tauri/capabilities:

{
  "identifier": "default",
  "description": "Default capability for the main window",
  "windows": ["main"],
  "permissions": [
    "dialog:allow-message"
  ]
}

Deprecated aliases:

The permissions dialog:allow-ask and dialog:allow-confirm still work but are now aliases for dialog:allow-message. They will be removed in v3. Use dialog:allow-message to cover all message dialog types.

Everything ready:

If you can import message, ask, and confirm without errors and the app compiles, the setup is correct.

Alert Dialogs

An alert dialog displays a short message and a single “OK” button. The user must dismiss it before continuing. It is the simplest of the three and is used for notifications that don’t require a decision.

From JavaScript

Import message and call it with the text you want to display. Optionally pass a title and a kind to set the icon. The function returns a boolean: true if the user clicked OK, false if they closed the window.

import { message } from "@tauri-apps/plugin-dialog";
function App() {
  const showAlert = async () => {
    const ok = await message("The file could not be saved.", {
      title: "Save Error",
      kind: "error",
    });
    console.log(ok ? "User acknowledged" : "Dialog dismissed");
  };
  return (
    <div>
      <button onClick={showAlert}>Show Alert</button>
    </div>
  );
}
export default App;

The kind option accepts "info", "warning", or "error". Each maps to a different system icon. On macOS and Windows the icon appears in the dialog itself; on some Linux desktop environments the icon may appear in the window title bar instead. If you omit kind, the dialog shows no icon.

Closing the dialog returns false:

If the user presses Escape or clicks the close button, message resolves to false. This is the only way to detect that they dismissed the dialog without engaging with it. If your flow depends on the user having seen the message, check the return value.

From Rust

The Rust side uses a builder pattern. Access the dialog API through app.dialog() and chain .message(), .title(), .kind(), then call .blocking_show() (synchronous) or .show(|answer| ...) (asynchronous callback).

use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
#[tauri::command]
fn show_alert(app: tauri::AppHandle) -> bool {
    app.dialog()
        .message("The configuration file is missing.")
        .title("Startup Error")
        .kind(MessageDialogKind::Error)
        .blocking_show()
}

blocking_show returns a bool with the same meaning: true for OK, false if the dialog was dismissed.

Never call blocking_show on the main thread:

In a non‑async Tauri command (no async keyword), blocking_show will freeze the entire window because the main event loop is halted. Only use it inside async commands, where Tauri runs the command on a thread pool. If you need a message dialog from a setup hook or a synchronous context, use the callback form:

app.dialog()
    .message("...")
    .show(|answer| {
        // handle answer here
    });

Confirmation Dialogs

A confirmation dialog presents a message with an “OK” button and a “Cancel” button. The typical pattern is: “Are you sure you want to do X?” where Cancel means “no, don’t do it.” It returns true for OK and false for Cancel (or dismissing the dialog).

From JavaScript

Use the confirm function. The signature is identical to message but the buttons are “OK” and “Cancel”.

import { confirm } from "@tauri-apps/plugin-dialog";
function DeleteButton() {
  const handleDelete = async () => {
    const ok = await confirm(
      "This will permanently delete the selected item.",
      { title: "Confirm Delete", kind: "warning" }
    );
    if (ok) {
      // proceed with deletion
    }
  };
  return <button onClick={handleDelete}>Delete Item</button>;
}
export default DeleteButton;

From Rust

The builder method .buttons() controls the button layout. Use MessageDialogButtons::OkCancel for the standard OK/Cancel pair, or MessageDialogButtons::OkCancelCustom to set custom labels.

use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
#[tauri::command]
async fn confirm_quit(app: tauri::AppHandle) -> bool {
    app.dialog()
        .message("Do you really want to quit?")
        .title("Quit")
        .buttons(MessageDialogButtons::OkCancel)
        .blocking_show()
}

Custom labels give the user more context about what each button does:

app.dialog()
    .message("Unsaved changes will be lost.")
    .title("Discard Changes")
    .buttons(MessageDialogButtons::OkCancelCustom("Discard".into(), "Keep Editing".into()))
    .blocking_show()

Ask Dialogs (Yes / No)

An ask dialog is structurally a confirmation, but the buttons say “Yes” and “No” instead of “OK” and “Cancel.” Use it when the question has a positive and negative answer that are both sensible actions — not just “proceed” vs “abort.”

From JavaScript

Call ask. It returns true for Yes and false for No (or dialog dismissal).

import { ask } from "@tauri-apps/plugin-dialog";
function OverwritePrompt() {
  const handleSave = async () => {
    const yes = await ask("A file with this name already exists. Overwrite?", {
      title: "File Exists",
      kind: "warning",
    });
    if (yes) {
      // overwrite
    }
  };
  return <button onClick={handleSave}>Save File</button>;
}
export default OverwritePrompt;

From Rust

Use MessageDialogButtons::YesNo or MessageDialogButtons::YesNoCustom for custom labels.

use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
#[tauri::command]
async fn ask_overwrite(app: tauri::AppHandle) -> bool {
    app.dialog()
        .message("Overwrite existing file?")
        .title("File Conflict")
        .buttons(MessageDialogButtons::YesNo)
        .blocking_show()
}

Dismissing is treated as No:

In all three dialog types, closing the window without pressing a button resolves to false. There is no way to distinguish “pressed Cancel” from “closed the dialog.” Design your flow so that false always means “do not proceed.”

Blocking vs Non‑blocking in Rust

The Rust API gives you two ways to get the result:

  • blocking_show() – waits for the user’s choice and then returns the boolean directly. Safe only in an async command.
  • show(|answer| { ... }) – takes a closure that runs when the dialog is dismissed. This form never blocks the calling thread and is safe everywhere.

If you are inside a synchronous setup hook (Builder::setup) or a synchronous command, use the callback form:

app.dialog()
    .message("Ready to launch?")
    .title("Launch")
    .buttons(MessageDialogButtons::YesNo)
    .show(|answer| {
        if answer {
            // proceed with launch
        }
    });

The JavaScript functions message, confirm, and ask are always asynchronous and never block the UI.

Permissions in Detail

Every message dialog call requires the dialog:allow-message permission. If it’s missing, the call will fail — in JavaScript you’ll get a rejected promise; in Rust, the dialog simply won’t appear and the return value will be false (or the callback will receive false).

Forgotten permissions cause silent failures:

If you call ask() and it always returns false without showing a dialog, check the browser console. You’ll likely see a permission denied error. Add "dialog:allow-message" to your capability file and rebuild.

Here is a complete capability file that grants message dialog access to every window:

{
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "dialog:allow-message"
  ]
}

If you have multiple windows and only want dialogs in one of them, list only that window in the windows array.

Common Mistakes

  • Calling blocking_show in a non‑async context. The window freezes and the app becomes unresponsive. If you need a message dialog in setup, use the callback .show(...).
  • Assuming message returns void. It returns a boolean. If you rely on the user having seen the message, check the return value.
  • Confusing confirm and ask. confirm uses OK/Cancel, ask uses Yes/No. They are semantically different — pick the one that matches the question’s wording.
  • Forgetting to add the npm package. If your frontend build complains that @tauri-apps/plugin-dialog cannot be found, run npm install @tauri-apps/plugin-dialog.
  • Not initializing the plugin in lib.rs. If .plugin(tauri_plugin_dialog::init()) is missing, the dialog API won’t be available on the Rust side and all calls will fail silently.

A Complete Example: Save‑Before‑Quit Flow

Here is a full workflow that asks the user to save changes before quitting. The frontend calls a Rust command that uses a blocking ask dialog, then performs the appropriate action.

Rust command (src-tauri/src/lib.rs):

use tauri::Manager;
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
#[tauri::command]
async fn save_before_quit(app: tauri::AppHandle) {
    let should_quit = app.dialog()
        .message("You have unsaved changes. Do you want to quit anyway?")
        .title("Unsaved Changes")
        .buttons(MessageDialogButtons::YesNo)
        .kind(tauri_plugin_dialog::MessageDialogKind::Warning)
        .blocking_show();
    if should_quit {
        // The user chose Yes — quit the app
        app.exit(0);
    }
    // If No, do nothing — the dialog closes and the app continues
}

React frontend (src/App.tsx):

import { invoke } from "@tauri-apps/api/core";
function QuitButton() {
  const handleQuit = () => {
    invoke("save_before_quit");
  };
  return <button onClick={handleQuit}>Quit</button>;
}
export default QuitButton;

When the user clicks “Quit,” the Rust command shows the native dialog. If they pick “Yes,” the app exits. If “No,” the dialog vanishes and the app stays open. This keeps the decision entirely on the native side, where it cannot be interrupted by a frontend crash or navigation.

Summary

Message dialogs are the simplest way to communicate with the user when you need their immediate attention or a yes/no decision. Tauri v2 gives you three dialog shapes — alert, confirm, ask — and lets you call them from JavaScript or Rust with identical semantics. The most important rule: always grant the dialog:allow-message permission, and never block the main thread with a synchronous dialog call.