$ cat /posts/tauri-20-notifications-desktop-push-notifications.md
[tags]Tauri 2.0

Tauri 2.0 Notifications Desktop Push Notifications

drwxr-xr-x2026-01-295 min0 views
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.

typescriptnotification_setup.ts
// 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.

typescriptbasic_notifications.ts
// 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.

typescriptnotification_events.ts
// 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.

rustnotification_queue.rs
// 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(&notification.title)
            .body(&notification.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

FeatureWindowsmacOSLinux
Notification CenterAction CenterNotification CenterVaries by DE
Action ButtonsSupportedSupportedLimited support
SoundSystem soundSystem soundDepends on DE
IconSupportedSupportedSupported
PersistenceStays in Action CenterStays until clearedUsually temporary
Urgency LevelsNot supportedNot exposedSupported
Group/CategoryBy appBy appBy 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.

rustmessaging_notifications.rs
// 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
Privacy Warning: Notification content may appear on lock screens visible to others nearby. Avoid displaying sensitive information like passwords, private messages, or financial details in notification text. Provide user setting to disable previews!

Next Steps

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!

$ cat /comments/ (0)

new_comment.sh

// Email hidden from public

>_

$ cat /comments/

// No comments found. Be the first!

[session] guest@{codershandbook}[timestamp] 2026

Navigation

Categories

Connect

Subscribe

// 2026 {Coders Handbook}. EOF.