Tauri 2.0 Notifications Desktop Push Notifications

Desktop notifications in Tauri 2.0 enable applications to send native system alerts appearing outside application window providing timely information, action reminders, status updates, and engagement triggers maintaining user awareness of important events even when application runs in background or user focuses on other tasks—essential feature for messaging apps, productivity tools, system monitors, and any application requiring real-time user communication. Tauri notifications leverage native OS notification systems including Windows Action Center, macOS Notification Center, and Linux notification daemons delivering familiar user experience with platform-specific styling, sounds, and behaviors while providing unified API abstracting platform differences enabling cross-platform notification implementation with single codebase. This comprehensive guide covers understanding notification architecture and platform differences across Windows, macOS, and Linux, requesting notification permissions with user consent handling, sending basic notifications with title and body text, adding notification icons with custom images, implementing action buttons for user responses, handling notification click events executing callbacks, managing notification persistence and timing, configuring notification sounds and urgency levels, building notification center with history tracking, and creating real-world examples including messaging app with message previews, task reminder with snooze actions, and system monitor with status alerts maintaining user engagement through timely contextual notifications. Mastering notification patterns enables building engaging desktop applications maintaining user awareness delivering important information at right time without interrupting workflow or demanding immediate attention unless explicitly required. Before proceeding, understand command creation and system tray integration.
Notification API Setup
Notification API requires enabling permissions in tauri.conf.json and requesting user consent before sending notifications. Understanding permission model integrated with application architecture enables building notification-aware applications respecting user preferences maintaining trust through transparent permission requests.
// src-tauri/tauri.conf.json
// Enable notification permissions
{
"tauri": {
"allowlist": {
"notification": {
"all": true
}
},
"bundle": {
"identifier": "com.example.myapp"
}
}
}
// src-tauri/Cargo.toml
// Notification dependencies included by default
[dependencies]
tauri = { version = "2.0", features = ["notification"] }
// Frontend: Check and request permissions
import {
isPermissionGranted,
requestPermission,
sendNotification,
} from "@tauri-apps/api/notification";
// Check if permission already granted
let permissionGranted = await isPermissionGranted();
if (!permissionGranted) {
// Request permission from user
const permission = await requestPermission();
permissionGranted = permission === "granted";
}
if (permissionGranted) {
// Permission granted, can send notifications
await sendNotification({
title: "Welcome!",
body: "Notifications are now enabled",
});
} else {
console.log("Notification permission denied");
}
// Rust: Send notifications from backend
use tauri::api::notification::Notification;
#[tauri::command]
fn send_notification_from_rust(
app: tauri::AppHandle,
title: String,
body: String,
) -> Result<(), String> {
Notification::new(&app.config().tauri.bundle.identifier)
.title(title)
.body(body)
.show()
.map_err(|e| e.to_string())?;
Ok(())
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
send_notification_from_rust
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Sending Basic Notifications
Basic notifications consist of title and body text appearing as native system alerts. Understanding notification structure enables building informative alerts delivering important information concisely maintaining user awareness without overwhelming with excessive details or frequent interruptions.
// Frontend: Send notifications from JavaScript
import { sendNotification } from "@tauri-apps/api/notification";
// Simple notification
await sendNotification({
title: "Task Complete",
body: "Your export has finished successfully",
});
// Notification with icon
await sendNotification({
title: "New Message",
body: "John: Hey, are you available?",
icon: "icons/message.png",
});
// Utility function for different notification types
class NotificationService {
static async info(title: string, body: string) {
await sendNotification({
title: `ℹ️ ${title}`,
body,
});
}
static async success(title: string, body: string) {
await sendNotification({
title: `✅ ${title}`,
body,
});
}
static async warning(title: string, body: string) {
await sendNotification({
title: `⚠️ ${title}`,
body,
});
}
static async error(title: string, body: string) {
await sendNotification({
title: `❌ ${title}`,
body,
});
}
}
// Usage
await NotificationService.success(
"Upload Complete",
"Your file has been uploaded successfully"
);
await NotificationService.warning(
"Low Storage",
"You have less than 10% storage remaining"
);
await NotificationService.error(
"Connection Failed",
"Unable to connect to the server"
);
// Rust: Advanced notification builder
use tauri::api::notification::Notification;
struct NotificationBuilder {
app_id: String,
}
impl NotificationBuilder {
fn new(app: &tauri::AppHandle) -> Self {
NotificationBuilder {
app_id: app.config().tauri.bundle.identifier.clone(),
}
}
fn send(
&self,
title: &str,
body: &str,
icon: Option<&str>,
) -> Result<(), String> {
let mut notification = Notification::new(&self.app_id)
.title(title)
.body(body);
if let Some(icon_path) = icon {
notification = notification.icon(icon_path);
}
notification.show().map_err(|e| e.to_string())
}
fn info(&self, title: &str, body: &str) -> Result<(), String> {
self.send(&format!("ℹ️ {}", title), body, None)
}
fn success(&self, title: &str, body: &str) -> Result<(), String> {
self.send(&format!("✅ {}", title), body, None)
}
fn warning(&self, title: &str, body: &str) -> Result<(), String> {
self.send(&format!("⚠️ {}", title), body, None)
}
fn error(&self, title: &str, body: &str) -> Result<(), String> {
self.send(&format!("❌ {}", title), body, None)
}
}
#[tauri::command]
fn notify_task_complete(
app: tauri::AppHandle,
task_name: String,
) -> Result<(), String> {
let notifier = NotificationBuilder::new(&app);
notifier.success(
"Task Complete",
&format!("{} has finished successfully", task_name),
)
}
#[tauri::command]
fn notify_error(
app: tauri::AppHandle,
error_msg: String,
) -> Result<(), String> {
let notifier = NotificationBuilder::new(&app);
notifier.error("Error Occurred", &error_msg)
}Handling Notification Events
Notification events enable responding to user interactions including clicks opening application windows or executing actions. Understanding event handling integrated with application logic enables building interactive notifications maintaining engagement through actionable alerts responding to user intent.
// Rust: Handle notification clicks with events
use tauri::Manager;
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize)]
struct NotificationData {
id: String,
action: String,
data: serde_json::Value,
}
#[tauri::command]
fn send_clickable_notification(
app: tauri::AppHandle,
title: String,
body: String,
notification_id: String,
) -> Result<(), String> {
use tauri::api::notification::Notification;
// Send notification
Notification::new(&app.config().tauri.bundle.identifier)
.title(&title)
.body(&body)
.show()
.map_err(|e| e.to_string())?;
// Store notification data for click handling
// In production, use proper state management
app.emit_all("notification-sent", NotificationData {
id: notification_id,
action: "show_window".to_string(),
data: serde_json::json!({ "title": title }),
})
.map_err(|e| e.to_string())?;
Ok(())
}
// Frontend: Listen for notification interactions
import { listen } from "@tauri-apps/api/event";
import { sendNotification } from "@tauri-apps/api/notification";
import { getCurrent } from "@tauri-apps/api/window";
// Send notification with action data
async function sendActionableNotification(
title: string,
body: string,
action: string,
data: any
) {
// Store action data in localStorage or state
const notificationId = `notif-${Date.now()}`;
localStorage.setItem(
notificationId,
JSON.stringify({ action, data })
);
// Send notification
await sendNotification({ title, body });
// On click, notification will activate window
// Handle action in window focus event
}
// Handle notification clicks by detecting window focus
const appWindow = getCurrent();
await appWindow.listen("tauri://focus", () => {
console.log("Window focused - possibly from notification click");
// Check for pending notification actions
const pendingActions = getPendingNotificationActions();
pendingActions.forEach((action) => {
handleNotificationAction(action);
});
});
function getPendingNotificationActions() {
// Retrieve stored notification actions
const actions = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key?.startsWith("notif-")) {
const data = JSON.parse(localStorage.getItem(key)!);
actions.push({ id: key, ...data });
localStorage.removeItem(key);
}
}
return actions;
}
function handleNotificationAction(action: any) {
switch (action.action) {
case "open_chat":
openChat(action.data.userId);
break;
case "show_task":
showTask(action.data.taskId);
break;
case "open_file":
openFile(action.data.filePath);
break;
default:
console.log("Unknown action:", action);
}
}
// Example: Message notification
async function notifyNewMessage(sender: string, message: string, userId: string) {
await sendActionableNotification(
`New message from ${sender}`,
message,
"open_chat",
{ userId }
);
}
// Example: Task reminder
async function notifyTaskDue(taskName: string, taskId: string) {
await sendActionableNotification(
"Task Due",
`${taskName} is due now`,
"show_task",
{ taskId }
);
}Notification Queue and Rate Limiting
Notification queue prevents overwhelming users with excessive alerts by implementing rate limiting and batching. Understanding queue management enables building respectful notification systems maintaining user engagement without causing notification fatigue through thoughtful timing and grouping of related alerts.
// Rust: Notification queue with rate limiting
use std::collections::VecDeque;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use tauri::api::notification::Notification;
use tauri::AppHandle;
struct NotificationQueue {
queue: Mutex<VecDeque<QueuedNotification>>,
last_sent: Mutex<Option<Instant>>,
min_interval: Duration,
}
struct QueuedNotification {
title: String,
body: String,
icon: Option<String>,
priority: NotificationPriority,
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
enum NotificationPriority {
Low = 0,
Normal = 1,
High = 2,
Urgent = 3,
}
impl NotificationQueue {
fn new(min_interval_secs: u64) -> Self {
NotificationQueue {
queue: Mutex::new(VecDeque::new()),
last_sent: Mutex::new(None),
min_interval: Duration::from_secs(min_interval_secs),
}
}
fn add(
&self,
title: String,
body: String,
icon: Option<String>,
priority: NotificationPriority,
) {
let mut queue = self.queue.lock().unwrap();
let notification = QueuedNotification {
title,
body,
icon,
priority,
};
// Insert based on priority
let insert_pos = queue
.iter()
.position(|n| n.priority < priority)
.unwrap_or(queue.len());
queue.insert(insert_pos, notification);
}
fn process(&self, app: &AppHandle) -> Result<(), String> {
let mut last_sent = self.last_sent.lock().unwrap();
let mut queue = self.queue.lock().unwrap();
// Check rate limit
if let Some(last) = *last_sent {
if last.elapsed() < self.min_interval {
return Ok(()); // Too soon, skip this cycle
}
}
// Send next notification
if let Some(notification) = queue.pop_front() {
let mut notif = Notification::new(
&app.config().tauri.bundle.identifier
)
.title(¬ification.title)
.body(¬ification.body);
if let Some(icon) = notification.icon {
notif = notif.icon(icon);
}
notif.show().map_err(|e| e.to_string())?;
*last_sent = Some(Instant::now());
}
Ok(())
}
fn clear(&self) {
let mut queue = self.queue.lock().unwrap();
queue.clear();
}
fn count(&self) -> usize {
let queue = self.queue.lock().unwrap();
queue.len()
}
}
#[tauri::command]
fn queue_notification(
app: tauri::AppHandle,
queue: tauri::State<NotificationQueue>,
title: String,
body: String,
priority: String,
) -> Result<(), String> {
let priority = match priority.as_str() {
"low" => NotificationPriority::Low,
"high" => NotificationPriority::High,
"urgent" => NotificationPriority::Urgent,
_ => NotificationPriority::Normal,
};
queue.add(title, body, None, priority);
queue.process(&app)?;
Ok(())
}
#[tauri::command]
fn get_pending_count(
queue: tauri::State<NotificationQueue>,
) -> usize {
queue.count()
}
// Initialize with background task
fn main() {
let notification_queue = NotificationQueue::new(3); // 3 seconds minimum
tauri::Builder::default()
.manage(notification_queue)
.setup(|app| {
let app_handle = app.handle();
let queue: tauri::State<NotificationQueue> = app_handle.state();
// Process queue periodically
std::thread::spawn(move || {
loop {
std::thread::sleep(Duration::from_secs(1));
let _ = queue.process(&app_handle);
}
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
queue_notification,
get_pending_count,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Platform-Specific Features
| Feature | Windows | macOS | Linux |
|---|---|---|---|
| Notification Center | Action Center | Notification Center | Varies by DE |
| Action Buttons | Supported | Supported | Limited support |
| Sound | System sound | System sound | Depends on DE |
| Icon | Supported | Supported | Supported |
| Persistence | Stays in Action Center | Stays until cleared | Usually temporary |
| Urgency Levels | Not supported | Not exposed | Supported |
| Group/Category | By app | By app | By category |
Real-World Example: Messaging App Notifications
Messaging application demonstrates complete notification integration with message previews, sender information, unread count updates, and click handling opening chat window. Understanding real-world implementation enables building engaging communication applications maintaining user awareness through timely notifications respecting privacy and user preferences.
// Complete messaging notification system
use tauri::{AppHandle, Manager};
use std::sync::Mutex;
use std::collections::HashMap;
struct MessagingNotifications {
unread_counts: Mutex<HashMap<String, u32>>,
notification_queue: NotificationQueue,
}
impl MessagingNotifications {
fn new() -> Self {
MessagingNotifications {
unread_counts: Mutex::new(HashMap::new()),
notification_queue: NotificationQueue::new(2),
}
}
}
#[tauri::command]
fn notify_new_message(
app: AppHandle,
messaging: tauri::State<MessagingNotifications>,
sender_id: String,
sender_name: String,
message: String,
preview_enabled: bool,
) -> Result<(), String> {
use tauri::api::notification::Notification;
// Update unread count
{
let mut counts = messaging.unread_counts.lock().unwrap();
let count = counts.entry(sender_id.clone()).or_insert(0);
*count += 1;
}
// Prepare notification
let body = if preview_enabled {
if message.len() > 100 {
format!("{}...", &message[..97])
} else {
message.clone()
}
} else {
"New message received".to_string()
};
let title = format!("Message from {}", sender_name);
// Send notification
messaging.notification_queue.add(
title,
body,
Some("icons/message.png".to_string()),
NotificationPriority::Normal,
);
messaging.notification_queue.process(&app)?;
// Update tray icon with unread count
let total_unread: u32 = {
let counts = messaging.unread_counts.lock().unwrap();
counts.values().sum()
};
if total_unread > 0 {
app.tray_handle()
.set_tooltip(&format!("{} unread messages", total_unread))
.ok();
}
Ok(())
}
#[tauri::command]
fn mark_messages_read(
app: AppHandle,
messaging: tauri::State<MessagingNotifications>,
sender_id: String,
) -> Result<(), String> {
// Clear unread count
{
let mut counts = messaging.unread_counts.lock().unwrap();
counts.remove(&sender_id);
}
// Update tray
let total_unread: u32 = {
let counts = messaging.unread_counts.lock().unwrap();
counts.values().sum()
};
if total_unread == 0 {
app.tray_handle()
.set_tooltip("No unread messages")
.ok();
} else {
app.tray_handle()
.set_tooltip(&format!("{} unread messages", total_unread))
.ok();
}
Ok(())
}
#[tauri::command]
fn get_unread_count(
messaging: tauri::State<MessagingNotifications>,
) -> u32 {
let counts = messaging.unread_counts.lock().unwrap();
counts.values().sum()
}
// Frontend integration
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
// Send message notification
async function onNewMessage(
senderId: string,
senderName: string,
message: string
) {
const previewEnabled = getNotificationPreferencesFromSettings();
await invoke("notify_new_message", {
sender_id: senderId,
sender_name: senderName,
message,
preview_enabled: previewEnabled,
});
}
// Mark as read when chat opened
async function openChat(senderId: string) {
await invoke("mark_messages_read", { sender_id: senderId });
// Open chat UI
}
// Get unread count for badge
async function updateUnreadBadge() {
const count = await invoke("get_unread_count");
updateUIBadge(count);
}Notification Best Practices
- Request Permission Early: Ask for notification permission during onboarding
- Respect User Preferences: Provide settings to control notification frequency
- Clear Concise Content: Use brief titles and bodies conveying essential information
- Appropriate Timing: Send notifications at relevant moments, not during quiet hours
- Rate Limiting: Implement delays between notifications preventing spam
- Priority Levels: Use urgent notifications sparingly for critical alerts only
- Actionable Content: Make notifications actionable with clear next steps
- Privacy Awareness: Avoid sensitive information in notification previews
- Group Similar: Batch related notifications together when possible
- Test Across Platforms: Verify notification appearance on all target OSes
Next Steps
- Global Shortcuts: Trigger actions with keyboard shortcuts
- System Tray: Integrate with tray icon
- Events: Coordinate with event system
- Commands: Send from backend with Rust commands
- Security: Secure with permission model
Conclusion
Mastering desktop notifications in Tauri 2.0 enables building engaging applications maintaining user awareness through timely contextual alerts appearing as native system notifications respecting platform conventions, user preferences, and privacy considerations delivering important information without interrupting workflow or causing notification fatigue through thoughtful timing and content. Notification system combines native OS integration leveraging Windows Action Center, macOS Notification Center, and Linux notification daemons with unified API abstracting platform differences, permission management respecting user consent, rich content supporting titles, bodies, and icons, event handling responding to user interactions, and queue management preventing overwhelming users maintaining engagement through respectful notification patterns. Understanding notification patterns including permission requests with transparent explanations, basic notifications with clear concise content, event handling opening relevant application views, queue management with rate limiting and priority, and real-world implementations like messaging apps with message previews establishes foundation for building professional desktop applications delivering timely information maintaining user engagement without demanding constant attention or causing annoyance through excessive interruptions. Your Tauri applications now possess powerful notification capabilities enabling features like real-time alerts for important events, actionable notifications with click handling, priority-based queuing, privacy-respecting content, and platform-native presentation delivering professional desktop experiences maintaining user awareness through thoughtful timely notifications integrated seamlessly with native OS notification systems!
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


