Tauri 2.0 System Tray Creating Background Applications

System tray integration in Tauri 2.0 enables desktop applications to run persistently in the background with always-accessible icon providing quick access to functionality, status indicators, context menus, and application controls without keeping main window visible—essential pattern for applications requiring continuous operation including messaging clients, music players, system monitors, and productivity tools maintaining user engagement while minimizing screen space. System tray combines native OS integration with customizable menu supporting items with icons, checkboxes, submenus, and separators, click actions opening windows or executing commands, dynamic icon updates showing application status, and tooltip providing current information creating professional desktop experience users expect from background applications. This comprehensive guide covers understanding system tray patterns and platform differences across Windows, macOS, and Linux, creating tray icons with custom images supporting light and dark themes, building context menus with actions, checkboxes, and hierarchical submenus, handling tray events including left click, right click, and double click actions, updating tray icon dynamically reflecting application state, managing tray tooltip showing current status, implementing menu item state with checkboxes and disabled items, and building real-world applications including music player with playback controls in tray, messaging app with unread count indicator, and system monitor displaying resource usage maintaining user awareness of background activity without intrusive interfaces. Mastering tray patterns enables building professional desktop applications providing seamless background operation maintaining quick access to features, status visibility through icon changes, and convenient controls through context menu without demanding constant attention from users. Before proceeding, understand command creation and window management.
System Tray Setup and Configuration
System tray requires configuration in tauri.conf.json specifying icon path, menu structure, and behavior preferences. Understanding tray configuration integrated with application architecture enables building background applications maintaining persistent system presence without requiring visible windows consuming taskbar space.
// src-tauri/tauri.conf.json
// Configure system tray
{
"tauri": {
"systemTray": {
"iconPath": "icons/tray-icon.png",
"iconAsTemplate": true, // macOS: adapts to light/dark
"menuOnLeftClick": false,
"title": "My App"
},
"bundle": {
"icon": [
"icons/32x32.png",
"icons/icon.ico",
"icons/icon.icns"
]
}
}
}
// Rust: Initialize system tray in main.rs
use tauri::{
CustomMenuItem, Manager, SystemTray, SystemTrayEvent,
SystemTrayMenu, SystemTrayMenuItem,
};
fn main() {
// Create menu items
let show = CustomMenuItem::new("show".to_string(), "Show Window");
let hide = CustomMenuItem::new("hide".to_string(), "Hide Window");
let quit = CustomMenuItem::new("quit".to_string(), "Quit");
// Build tray menu
let tray_menu = SystemTrayMenu::new()
.add_item(show)
.add_item(hide)
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(quit);
// Create system tray
let system_tray = SystemTray::new()
.with_menu(tray_menu)
.with_tooltip("My Application");
tauri::Builder::default()
.system_tray(system_tray)
.on_system_tray_event(|app, event| match event {
SystemTrayEvent::LeftClick {
position: _,
size: _,
..
} => {
println!("Left click on tray icon");
// Toggle window visibility
let window = app.get_window("main").unwrap();
if window.is_visible().unwrap() {
window.hide().unwrap();
} else {
window.show().unwrap();
}
}
SystemTrayEvent::RightClick {
position: _,
size: _,
..
} => {
println!("Right click - show context menu");
}
SystemTrayEvent::DoubleClick {
position: _,
size: _,
..
} => {
println!("Double click on tray");
let window = app.get_window("main").unwrap();
window.show().unwrap();
window.set_focus().unwrap();
}
SystemTrayEvent::MenuItemClick { id, .. } => {
match id.as_str() {
"show" => {
let window = app.get_window("main").unwrap();
window.show().unwrap();
}
"hide" => {
let window = app.get_window("main").unwrap();
window.hide().unwrap();
}
"quit" => {
std::process::exit(0);
}
_ => {}
}
}
_ => {}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Building Tray Context Menus
Tray context menus provide primary interface for background applications with items supporting text labels, keyboard shortcuts, icons, checkable states, disabled states, and hierarchical submenus. Understanding menu structure enables building intuitive interfaces providing quick access to application features maintaining consistent patterns users expect from native desktop applications.
// Rust: Advanced menu with submenus and checkboxes
use tauri::{
CustomMenuItem, SystemTrayMenu, SystemTrayMenuItem,
SystemTraySubmenu,
};
fn create_tray_menu() -> SystemTrayMenu {
// Main window controls
let show = CustomMenuItem::new("show", "Show Window");
let hide = CustomMenuItem::new("hide", "Hide Window");
let minimize = CustomMenuItem::new("minimize", "Minimize");
// Checkable items for settings
let autostart = CustomMenuItem::new("autostart", "Start on Login")
.selected(); // Initially checked
let notifications = CustomMenuItem::new("enable_notifications",
"Enable Notifications")
.selected();
// Disabled item (for status)
let status = CustomMenuItem::new("status", "Status: Connected")
.disabled();
// Settings submenu
let preferences = CustomMenuItem::new("preferences", "Preferences...");
let about = CustomMenuItem::new("about", "About");
let check_updates = CustomMenuItem::new("check_updates",
"Check for Updates");
let settings_submenu = SystemTraySubmenu::new(
"Settings",
SystemTrayMenu::new()
.add_item(preferences)
.add_item(about)
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(check_updates),
);
// Build complete menu
SystemTrayMenu::new()
.add_item(status)
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(show)
.add_item(hide)
.add_item(minimize)
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(autostart)
.add_item(notifications)
.add_native_item(SystemTrayMenuItem::Separator)
.add_submenu(settings_submenu)
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit", "Quit"))
}
// Handle menu clicks with state management
use std::sync::Mutex;
use tauri::State;
struct AppState {
autostart_enabled: Mutex<bool>,
notifications_enabled: Mutex<bool>,
}
fn handle_menu_event(
app: &tauri::AppHandle,
event: SystemTrayEvent,
state: State<AppState>,
) {
if let SystemTrayEvent::MenuItemClick { id, .. } = event {
match id.as_str() {
"show" => {
let window = app.get_window("main").unwrap();
window.show().unwrap();
window.set_focus().unwrap();
}
"hide" => {
let window = app.get_window("main").unwrap();
window.hide().unwrap();
}
"minimize" => {
let window = app.get_window("main").unwrap();
window.minimize().unwrap();
}
"autostart" => {
// Toggle autostart
let mut enabled = state.autostart_enabled.lock().unwrap();
*enabled = !*enabled;
// Update menu item checkbox
let item = app.tray_handle()
.get_item(&id);
item.set_selected(*enabled).unwrap();
println!("Autostart: {}", *enabled);
}
"enable_notifications" => {
// Toggle notifications
let mut enabled = state.notifications_enabled.lock().unwrap();
*enabled = !*enabled;
let item = app.tray_handle().get_item(&id);
item.set_selected(*enabled).unwrap();
println!("Notifications: {}", *enabled);
}
"preferences" => {
// Open preferences window
if let Some(window) = app.get_window("preferences") {
window.show().unwrap();
} else {
// Create preferences window
tauri::WindowBuilder::new(
app,
"preferences",
tauri::WindowUrl::App("preferences.html".into()),
)
.title("Preferences")
.inner_size(600.0, 400.0)
.build()
.unwrap();
}
}
"about" => {
// Show about dialog
app.emit_all("show-about", ()).unwrap();
}
"check_updates" => {
app.emit_all("check-updates", ()).unwrap();
}
"quit" => {
std::process::exit(0);
}
_ => {}
}
}
}
// Initialize with state
fn main() {
let app_state = AppState {
autostart_enabled: Mutex::new(true),
notifications_enabled: Mutex::new(true),
};
let tray_menu = create_tray_menu();
let system_tray = SystemTray::new().with_menu(tray_menu);
tauri::Builder::default()
.manage(app_state)
.system_tray(system_tray)
.on_system_tray_event(|app, event| {
let state: State<AppState> = app.state();
handle_menu_event(app, event, state);
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Dynamic Tray Updates
Dynamic tray updates enable changing icon, tooltip, and menu structure at runtime reflecting application state changes. Understanding dynamic updates enables building responsive tray interfaces showing real-time status through icon changes, displaying current information in tooltips, and updating menu items reflecting available actions maintaining user awareness of application state without requiring window visibility.
// Rust: Dynamic tray icon and tooltip updates
use tauri::{AppHandle, Icon, Manager};
use std::path::PathBuf;
// Update tray icon
#[tauri::command]
fn update_tray_icon(
app: AppHandle,
icon_name: String,
) -> Result<(), String> {
let icon_path = format!("icons/{}.png", icon_name);
let icon = Icon::File(PathBuf::from(icon_path));
app.tray_handle()
.set_icon(icon)
.map_err(|e| e.to_string())?;
Ok(())
}
// Update tray tooltip
#[tauri::command]
fn update_tray_tooltip(
app: AppHandle,
tooltip: String,
) -> Result<(), String> {
app.tray_handle()
.set_tooltip(&tooltip)
.map_err(|e| e.to_string())?;
Ok(())
}
// Update tray title (macOS only)
#[tauri::command]
fn update_tray_title(
app: AppHandle,
title: String,
) -> Result<(), String> {
#[cfg(target_os = "macos")]
{
app.tray_handle()
.set_title(&title)
.map_err(|e| e.to_string())?;
}
Ok(())
}
// Update entire menu dynamically
#[tauri::command]
fn update_tray_menu(
app: AppHandle,
is_playing: bool,
song_title: String,
) -> Result<(), String> {
use tauri::{CustomMenuItem, SystemTrayMenu, SystemTrayMenuItem};
// Create dynamic menu based on state
let play_pause = if is_playing {
CustomMenuItem::new("pause", "⏸ Pause")
} else {
CustomMenuItem::new("play", "▶ Play")
};
let current_song = CustomMenuItem::new(
"current_song",
format!("🎵 {}", song_title),
)
.disabled();
let new_menu = SystemTrayMenu::new()
.add_item(current_song)
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(play_pause)
.add_item(CustomMenuItem::new("next", "⏭ Next"))
.add_item(CustomMenuItem::new("previous", "⏮ Previous"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("show", "Show Player"))
.add_item(CustomMenuItem::new("quit", "Quit"));
app.tray_handle()
.set_menu(new_menu)
.map_err(|e| e.to_string())?;
Ok(())
}
// Status indicator with colored icons
#[tauri::command]
fn set_connection_status(
app: AppHandle,
status: String,
) -> Result<(), String> {
let (icon_name, tooltip_text) = match status.as_str() {
"online" => ("tray-online", "Connected"),
"connecting" => ("tray-connecting", "Connecting..."),
"offline" => ("tray-offline", "Disconnected"),
_ => ("tray-default", "Unknown"),
};
// Update icon
let icon = Icon::File(PathBuf::from(
format!("icons/{}.png", icon_name)
));
app.tray_handle().set_icon(icon).map_err(|e| e.to_string())?;
// Update tooltip
app.tray_handle()
.set_tooltip(tooltip_text)
.map_err(|e| e.to_string())?;
Ok(())
}
// Badge counter (for notifications)
#[tauri::command]
fn update_badge_count(
app: AppHandle,
count: u32,
) -> Result<(), String> {
if count > 0 {
let title = format!("({})", count);
let tooltip = format!("{} unread messages", count);
#[cfg(target_os = "macos")]
app.tray_handle()
.set_title(&title)
.map_err(|e| e.to_string())?;
app.tray_handle()
.set_tooltip(&tooltip)
.map_err(|e| e.to_string())?;
// Use notification icon
let icon = Icon::File(PathBuf::from("icons/tray-notification.png"));
app.tray_handle().set_icon(icon).map_err(|e| e.to_string())?;
} else {
#[cfg(target_os = "macos")]
app.tray_handle()
.set_title("")
.map_err(|e| e.to_string())?;
app.tray_handle()
.set_tooltip("My App")
.map_err(|e| e.to_string())?;
// Use default icon
let icon = Icon::File(PathBuf::from("icons/tray-icon.png"));
app.tray_handle().set_icon(icon).map_err(|e| e.to_string())?;
}
Ok(())
}
// Frontend: Update tray from JavaScript
import { invoke } from "@tauri-apps/api/core";
// Change connection status
await invoke("set_connection_status", { status: "online" });
// Update badge count
await invoke("update_badge_count", { count: 5 });
// Update music player tray
await invoke("update_tray_menu", {
is_playing: true,
song_title: "Song Name - Artist",
});
// Update tooltip
await invoke("update_tray_tooltip", {
tooltip: "App Name - Status Info",
});Platform-Specific Considerations
| Feature | Windows | macOS | Linux |
|---|---|---|---|
| Icon Location | System tray (bottom-right) | Menu bar (top-right) | Varies by DE |
| Template Icons | Not supported | Supported, adapts to theme | Not supported |
| Title Text | Not supported | Supported | Not supported |
| Icon Size | 16x16 | 22x22 (44x44 @2x) | 22x22 or 24x24 |
| Tooltip | Supported | Supported | Supported |
| Left Click | Custom action | Shows menu by default | Custom action |
| Right Click | Shows menu | Shows menu | Shows menu |
Real-World Example: Music Player Tray
Music player application demonstrates complete tray integration with playback controls in context menu, dynamic icon showing play/pause state, current song display in menu, and seamless window management. Understanding real-world implementation enables building professional background applications maintaining full functionality through tray interface without requiring visible window for basic operations.
// Complete music player tray implementation
use tauri::{
AppHandle, CustomMenuItem, Manager, State, SystemTray,
SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
};
use std::sync::Mutex;
#[derive(Clone, Debug)]
struct PlayerState {
is_playing: bool,
current_song: String,
current_artist: String,
}
struct MusicPlayer {
state: Mutex<PlayerState>,
}
impl MusicPlayer {
fn new() -> Self {
MusicPlayer {
state: Mutex::new(PlayerState {
is_playing: false,
current_song: "No track playing".to_string(),
current_artist: String::new(),
}),
}
}
fn update_tray_menu(&self, app: &AppHandle) -> Result<(), String> {
let state = self.state.lock().unwrap();
let track_info = if state.current_song == "No track playing" {
state.current_song.clone()
} else {
format!("{} - {}", state.current_song, state.current_artist)
};
let play_pause_label = if state.is_playing {
"⏸ Pause"
} else {
"▶ Play"
};
let menu = SystemTrayMenu::new()
.add_item(
CustomMenuItem::new("track_info", track_info)
.disabled()
)
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("play_pause", play_pause_label))
.add_item(CustomMenuItem::new("next", "⏭ Next Track"))
.add_item(CustomMenuItem::new("previous", "⏮ Previous Track"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("show_player", "Show Player"))
.add_item(CustomMenuItem::new("quit", "Quit"));
app.tray_handle()
.set_menu(menu)
.map_err(|e| e.to_string())?;
// Update tooltip
let tooltip = if state.is_playing {
format!("Playing: {} - {}", state.current_song, state.current_artist)
} else {
"Music Player - Paused".to_string()
};
app.tray_handle()
.set_tooltip(&tooltip)
.map_err(|e| e.to_string())?;
Ok(())
}
}
#[tauri::command]
fn play_song(
app: AppHandle,
player: State<MusicPlayer>,
song: String,
artist: String,
) -> Result<(), String> {
{
let mut state = player.state.lock().unwrap();
state.is_playing = true;
state.current_song = song;
state.current_artist = artist;
}
player.update_tray_menu(&app)?;
Ok(())
}
#[tauri::command]
fn toggle_playback(
app: AppHandle,
player: State<MusicPlayer>,
) -> Result<bool, String> {
let is_playing = {
let mut state = player.state.lock().unwrap();
state.is_playing = !state.is_playing;
state.is_playing
};
player.update_tray_menu(&app)?;
Ok(is_playing)
}
fn handle_music_tray_event(
app: &AppHandle,
event: SystemTrayEvent,
player: State<MusicPlayer>,
) {
match event {
SystemTrayEvent::DoubleClick { .. } => {
if let Some(window) = app.get_window("main") {
window.show().unwrap();
window.set_focus().unwrap();
}
}
SystemTrayEvent::MenuItemClick { id, .. } => {
match id.as_str() {
"play_pause" => {
toggle_playback(app.clone(), player.clone()).ok();
app.emit_all("playback-toggled", ()).unwrap();
}
"next" => {
app.emit_all("next-track", ()).unwrap();
}
"previous" => {
app.emit_all("previous-track", ()).unwrap();
}
"show_player" => {
if let Some(window) = app.get_window("main") {
window.show().unwrap();
window.set_focus().unwrap();
}
}
"quit" => {
std::process::exit(0);
}
_ => {}
}
}
_ => {}
}
}
fn main() {
let player = MusicPlayer::new();
let menu = SystemTrayMenu::new()
.add_item(
CustomMenuItem::new("track_info", "No track playing")
.disabled()
)
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("play_pause", "▶ Play"))
.add_item(CustomMenuItem::new("quit", "Quit"));
let system_tray = SystemTray::new().with_menu(menu);
tauri::Builder::default()
.manage(player)
.system_tray(system_tray)
.on_system_tray_event(|app, event| {
let player: State<MusicPlayer> = app.state();
handle_music_tray_event(app, event, player);
})
.invoke_handler(tauri::generate_handler![
play_song,
toggle_playback,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}System Tray Best Practices
- Descriptive Tooltip: Provide clear tooltip text showing app name and current status
- Logical Menu Order: Place most common actions at top of menu
- Visual Separators: Group related items with separator lines
- Status Display: Show current state as disabled menu item
- Quick Window Access: Include show/hide window options
- Always Include Quit: Provide clear exit option in menu
- Dynamic Updates: Update icon and menu reflecting state changes
- Platform Icons: Use template icons on macOS, colored on Windows
- Double-Click Action: Show main window on double-click
- Keyboard Shortcuts: Consider adding hotkeys for common actions
Next Steps
- Notifications: Add desktop alerts with notification API
- Global Shortcuts: Register hotkeys with global shortcuts
- Window Management: Control windows from tray menu
- Events: Communicate with event system
- Commands: Implement actions with Rust commands
Conclusion
Mastering system tray integration in Tauri 2.0 enables building professional background applications providing persistent system presence through always-visible icon, quick access to functionality through context menu, status visibility through dynamic icon updates, and seamless window management maintaining user engagement without demanding constant attention from users. System tray combines native OS integration adapting to platform conventions with customizable menu supporting items, checkboxes, submenus, and separators, click event handling responding to user interactions, dynamic updates reflecting application state changes, and tooltip providing current information creating professional desktop experience users expect from background applications. Understanding tray patterns including menu organization with logical grouping, status indicators showing application state, window visibility toggling, platform-specific behaviors handling OS differences, and real-world implementations like music players with playback controls establishes foundation for professional desktop application development delivering polished experiences maintaining functionality without visible windows consuming screen space. Your Tauri applications now possess powerful system tray capabilities enabling features like background operation with persistent presence, quick controls through context menu, status monitoring through icon changes, and window management through tray interactions delivering professional desktop experiences with seamless OS integration maintaining user awareness without intrusive interfaces!
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


