Send Desktop Notifications with Tauri | React + Rust Tutorial
Video: Send Desktop Notifications with Tauri | React + Rust Tutorial by Taught by Celeste AI - AI Coding Coach
tauri-plugin-notificationplus permission requests, plus a simpleNotification.title("...").body("...").show()pattern.
Native desktop notifications — the small banner that pops up at the corner of your screen when something interesting happens — are part of every modern app's vocabulary. Tauri exposes them via the notification plugin, which works on all three desktop platforms (macOS, Windows, Linux).
Install the plugin
cd src-tauri
cargo add tauri-plugin-notification
npm install @tauri-apps/plugin-notification
Register in lib.rs:
tauri::Builder::default()
.plugin(tauri_plugin_notification::init())
// ...
Add to capabilities/default.json:
"permissions": [
"core:default",
"notification:default"
]
Permissions: ask the user
Notifications require user consent on macOS and Windows. Check and request:
import {
isPermissionGranted,
requestPermission,
sendNotification,
} from "@tauri-apps/plugin-notification";
async function notify(title: string, body: string) {
let granted = await isPermissionGranted();
if (!granted) {
const permission = await requestPermission();
granted = permission === "granted";
}
if (granted) {
sendNotification({ title, body });
}
}
The first call pops a system permission dialog. After that, the user's choice is remembered — subsequent calls don't ask again unless the user revokes the permission via system settings.
Sending from the frontend
import { sendNotification } from "@tauri-apps/plugin-notification";
sendNotification({
title: "Build Complete",
body: "Your project finished compiling.",
});
The simplest form. A title and a body. The OS shows a banner styled in its native idiom — not your CSS.
Sending from Rust
use tauri_plugin_notification::NotificationExt;
#[tauri::command]
fn notify_user(app: tauri::AppHandle, title: String, body: String) -> Result<(), String> {
app.notification()
.builder()
.title(title)
.body(body)
.show()
.map_err(|e| e.to_string())
}
Useful when the trigger is server-side or the message is generated on the Rust side.
Custom icon
sendNotification({
title: "Hello",
body: "This is a notification with an icon.",
icon: "/path/to/icon.png",
});
Or, on the Rust side, the icon defaults to the app icon. For a custom icon, configure it via the plugin's builder.
Sound
sendNotification({
title: "New message",
body: "From Alice",
sound: "default",
});
sound: "default" plays the OS's default notification sound. On macOS, you can also use named sounds: "Funk", "Glass", "Hero", etc.
When the user clicks a notification
Tauri 2 fires events when the user interacts with a notification:
import { onAction } from "@tauri-apps/plugin-notification";
onAction((notification) => {
console.log("User clicked notification", notification);
});
Useful for "take me to the relevant screen" UX. Pair with deep linking — the notification carries an ID, the action handler navigates to the matching record.
Throttling
Don't fire ten notifications in five seconds. The OS often coalesces them, but more importantly, users find notification spam disrespectful and disable the app's permission. Send one summary instead of N individual notifications when something fires repeatedly.
Permission patterns
Always check before sending:
async function safeNotify(title: string, body: string) {
if (!(await isPermissionGranted())) return;
sendNotification({ title, body });
}
This way silent failures (user denied permission, system disabled them) don't propagate to your error logs.
Permission UX
The first time you call requestPermission() triggers a system dialog. That dialog appears once. If the user clicks "Don't Allow," subsequent requestPermission calls return the previous answer immediately — they don't ask again.
For a graceful first-time experience:
- Don't ask on first launch. Wait until you have a reason.
- Explain why you want notification permission in your own UI before triggering the system prompt.
- Provide a settings toggle that respects the user's choice.
Practical example: build-complete notification
function BuildButton() {
async function startBuild() {
await invoke("compile_project");
await notify("Build Complete", "Your project finished compiling.");
}
async function notify(title: string, body: string) {
if (!(await isPermissionGranted())) {
await requestPermission();
}
if (await isPermissionGranted()) {
sendNotification({ title, body });
}
}
return <button onClick={startBuild}>Build</button>;
}
The user clicks Build. The Rust command compiles (could take a while). When done, a notification appears. If the user is in a different window, they see the banner and click to come back.
Common mistakes
Asking for permission on app launch. Annoying. Wait until you actually need to send something.
Sending from a tight loop. OS-level rate limiting kicks in. Coalesce or throttle.
Not registering the plugin. Calling sendNotification without .plugin(tauri_plugin_notification::init()) in the builder fails silently.
Forgetting capabilities. ACL error if notification:default isn't granted.
Using long titles. The OS truncates. Keep titles under ~50 characters; bodies under ~200.
What's next
Next lesson: animated buttons and visual polish. A small detour into the front-end side — small motion details that make your app feel alive.
Recap
tauri-plugin-notification for desktop notifications. Check isPermissionGranted and requestPermission before sending. sendNotification({ title, body }) from JS or app.notification().builder().title(...).body(...).show() from Rust. Throttle to avoid spam. Capability notification:default is required.
Next: animated buttons.