adding-tauri-system-tray

Tauri System Tray Implementation

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "adding-tauri-system-tray" with this command: npx skills add dchuk/claude-code-tauri-skills/dchuk-claude-code-tauri-skills-adding-tauri-system-tray

Tauri System Tray Implementation

This skill covers implementing system tray (notification area) functionality in Tauri v2 applications.

Configuration

Enable the tray-icon feature in src-tauri/Cargo.toml :

[dependencies] tauri = { version = "2", features = ["tray-icon"] }

Basic Tray Setup

Create a tray icon in src-tauri/src/lib.rs :

use tauri::tray::TrayIconBuilder;

#[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .setup(|app| { // Use TrayIconBuilder::with_id() to reference the tray later let tray = TrayIconBuilder::with_id(app, "main-tray") .icon(app.default_window_icon().unwrap().clone()) .tooltip("My Tauri App") .build(app)?; Ok(()) }) .run(tauri::generate_context!()) .expect("error while running tauri application"); }

Tray Menu

Basic Menu with Items

use tauri::{ menu::{Menu, MenuItem}, tray::TrayIconBuilder, };

#[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .setup(|app| { let show_item = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?; let hide_item = MenuItem::with_id(app, "hide", "Hide Window", true, None::<&str>)?; let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;

        let menu = Menu::with_items(app, &#x26;[&#x26;show_item, &#x26;hide_item, &#x26;quit_item])?;

        let tray = TrayIconBuilder::new()
            .icon(app.default_window_icon().unwrap().clone())
            .menu(&#x26;menu)
            .menu_on_left_click(false)  // Only show menu on right-click
            .build(app)?;

        Ok(())
    })
    .run(tauri::generate_context!())
    .expect("error while running tauri application");

}

Menu with Separators and Submenus

use tauri::{ menu::{Menu, MenuItem, PredefinedMenuItem, Submenu}, tray::TrayIconBuilder, };

#[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .setup(|app| { let option1 = MenuItem::with_id(app, "option1", "Option 1", true, None::<&str>)?; let option2 = MenuItem::with_id(app, "option2", "Option 2", true, None::<&str>)?; let options_submenu = Submenu::with_items(app, "Options", true, &[&option1, &option2])?;

        let show_item = MenuItem::with_id(app, "show", "Show Window", true, None::&#x3C;&#x26;str>)?;
        let separator = PredefinedMenuItem::separator(app)?;
        let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::&#x3C;&#x26;str>)?;

        let menu = Menu::with_items(
            app,
            &#x26;[&#x26;show_item, &#x26;options_submenu, &#x26;separator, &#x26;quit_item],
        )?;

        let tray = TrayIconBuilder::new()
            .icon(app.default_window_icon().unwrap().clone())
            .menu(&#x26;menu)
            .build(app)?;

        Ok(())
    })
    .run(tauri::generate_context!())
    .expect("error while running tauri application");

}

Handling Tray Events

Menu Item Events

use tauri::{ menu::{Menu, MenuItem}, tray::TrayIconBuilder, Manager, };

#[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .setup(|app| { let show_item = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?; let hide_item = MenuItem::with_id(app, "hide", "Hide Window", true, None::<&str>)?; let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; let menu = Menu::with_items(app, &[&show_item, &hide_item, &quit_item])?;

        let tray = TrayIconBuilder::new()
            .icon(app.default_window_icon().unwrap().clone())
            .menu(&#x26;menu)
            .on_menu_event(|app, event| {
                match event.id.as_ref() {
                    "show" => {
                        if let Some(window) = app.get_webview_window("main") {
                            let _ = window.show();
                            let _ = window.set_focus();
                        }
                    }
                    "hide" => {
                        if let Some(window) = app.get_webview_window("main") {
                            let _ = window.hide();
                        }
                    }
                    "quit" => app.exit(0),
                    _ => println!("Unhandled menu item: {:?}", event.id),
                }
            })
            .build(app)?;

        Ok(())
    })
    .run(tauri::generate_context!())
    .expect("error while running tauri application");

}

Tray Icon Mouse Events

use tauri::{ tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, Manager, };

#[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .setup(|app| { let tray = TrayIconBuilder::new() .icon(app.default_window_icon().unwrap().clone()) .on_tray_icon_event(|tray, event| { match event { TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } => { let app = tray.app_handle(); if let Some(window) = app.get_webview_window("main") { let _ = window.unminimize(); let _ = window.show(); let _ = window.set_focus(); } } TrayIconEvent::DoubleClick { button: MouseButton::Left, .. } => { let app = tray.app_handle(); if let Some(window) = app.get_webview_window("main") { if window.is_visible().unwrap_or(false) { let _ = window.hide(); } else { let _ = window.show(); let _ = window.set_focus(); } } } TrayIconEvent::Enter { .. } => println!("Mouse entered tray"), TrayIconEvent::Leave { .. } => println!("Mouse left tray"), _ => {} } }) .build(app)?;

        Ok(())
    })
    .run(tauri::generate_context!())
    .expect("error while running tauri application");

}

Note: Enter , Move , and Leave events are not supported on Linux.

Updating Tray at Runtime

Update Icon and Tooltip

use tauri::{image::Image, tray::TrayIconBuilder, Manager};

#[tauri::command] fn update_tray_icon(app: tauri::AppHandle, icon_path: String) -> Result<(), String> { if let Some(tray) = app.tray_by_id("main-tray") { let icon = Image::from_path(&icon_path).map_err(|e| e.to_string())?; tray.set_icon(Some(icon)).map_err(|e| e.to_string())?; } Ok(()) }

#[tauri::command] fn update_tray_tooltip(app: tauri::AppHandle, tooltip: String) -> Result<(), String> { if let Some(tray) = app.tray_by_id("main-tray") { tray.set_tooltip(Some(&tooltip)).map_err(|e| e.to_string())?; } Ok(()) }

Update Menu Items Dynamically

use std::sync::Mutex; use tauri::{ menu::{Menu, MenuItem, MenuItemKind}, tray::TrayIconBuilder, Manager, };

struct AppState { menu: Mutex<Option<Menu<tauri::Wry>>>, }

#[tauri::command] fn toggle_menu_item(app: tauri::AppHandle, item_id: String, enabled: bool) -> Result<(), String> { let state = app.state::<AppState>(); if let Some(menu) = state.menu.lock().unwrap().as_ref() { if let Some(MenuItemKind::MenuItem(item)) = menu.get(&item_id) { item.set_enabled(enabled).map_err(|e| e.to_string())?; } } Ok(()) }

#[tauri::command] fn update_menu_text(app: tauri::AppHandle, item_id: String, text: String) -> Result<(), String> { let state = app.state::<AppState>(); if let Some(menu) = state.menu.lock().unwrap().as_ref() { if let Some(MenuItemKind::MenuItem(item)) = menu.get(&item_id) { item.set_text(&text).map_err(|e| e.to_string())?; } } Ok(()) }

Replace Entire Menu

use tauri::{menu::{Menu, MenuItem}, Manager};

#[tauri::command] fn set_connected_menu(app: tauri::AppHandle) -> Result<(), String> { if let Some(tray) = app.tray_by_id("main-tray") { let disconnect = MenuItem::with_id(&app, "disconnect", "Disconnect", true, None::<&str>) .map_err(|e| e.to_string())?; let status = MenuItem::with_id(&app, "status", "Connected", false, None::<&str>) .map_err(|e| e.to_string())?; let quit = MenuItem::with_id(&app, "quit", "Quit", true, None::<&str>) .map_err(|e| e.to_string())?;

    let menu = Menu::with_items(&#x26;app, &#x26;[&#x26;status, &#x26;disconnect, &#x26;quit])
        .map_err(|e| e.to_string())?;
    tray.set_menu(Some(menu)).map_err(|e| e.to_string())?;
}
Ok(())

}

Complete Example

use std::sync::Mutex; use tauri::{ menu::{Menu, MenuItem, PredefinedMenuItem}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, Manager, };

struct TrayState { is_paused: Mutex<bool>, }

#[tauri::command] fn get_tray_status(state: tauri::State<TrayState>) -> bool { *state.is_paused.lock().unwrap() }

#[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .manage(TrayState { is_paused: Mutex::new(false) }) .setup(|app| { let show = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?; let hide = MenuItem::with_id(app, "hide", "Hide Window", true, None::<&str>)?; let sep = PredefinedMenuItem::separator(app)?; let pause = MenuItem::with_id(app, "pause", "Pause", true, None::<&str>)?; let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;

        let menu = Menu::with_items(app, &#x26;[&#x26;show, &#x26;hide, &#x26;sep, &#x26;pause, &#x26;quit])?;

        let _tray = TrayIconBuilder::with_id(app, "main-tray")
            .icon(app.default_window_icon().unwrap().clone())
            .tooltip("My Tauri App - Running")
            .menu(&#x26;menu)
            .menu_on_left_click(false)
            .on_menu_event(|app, event| {
                match event.id.as_ref() {
                    "show" => {
                        if let Some(w) = app.get_webview_window("main") {
                            let _ = w.show();
                            let _ = w.set_focus();
                        }
                    }
                    "hide" => {
                        if let Some(w) = app.get_webview_window("main") {
                            let _ = w.hide();
                        }
                    }
                    "pause" => {
                        let state = app.state::&#x3C;TrayState>();
                        let mut paused = state.is_paused.lock().unwrap();
                        *paused = !*paused;
                        if let Some(tray) = app.tray_by_id("main-tray") {
                            let tip = if *paused { "Paused" } else { "Running" };
                            let _ = tray.set_tooltip(Some(tip));
                        }
                    }
                    "quit" => app.exit(0),
                    _ => {}
                }
            })
            .on_tray_icon_event(|tray, event| {
                if let TrayIconEvent::Click {
                    button: MouseButton::Left,
                    button_state: MouseButtonState::Up, ..
                } = event {
                    let app = tray.app_handle();
                    if let Some(w) = app.get_webview_window("main") {
                        if w.is_visible().unwrap_or(false) {
                            let _ = w.hide();
                        } else {
                            let _ = w.show();
                            let _ = w.set_focus();
                        }
                    }
                }
            })
            .build(app)?;

        Ok(())
    })
    .invoke_handler(tauri::generate_handler![get_tray_status])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");

}

Platform Notes

Platform Support

Windows Full support for all tray events

macOS Full support for all tray events

Linux Enter , Move , Leave events not supported

Troubleshooting

Tray icon not appearing:

  • Ensure tray-icon feature is enabled in Cargo.toml

  • Verify the icon is valid and accessible

  • Check that build() is called and result is stored

Menu not showing:

  • Confirm menu is attached with .menu(&menu)

  • Check menu_on_left_click setting

  • Verify menu items are created correctly

Events not firing:

  • Ensure event handlers are attached before build()

  • Check pattern matching in event handlers

  • Verify tray ID matches when using tray_by_id()

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

integrating-tauri-js-frontends

No summary provided by upstream source.

Repository SourceNeeds Review
-138
dchuk
Coding

configuring-tauri-permissions

No summary provided by upstream source.

Repository SourceNeeds Review
-116
dchuk
Coding

understanding-tauri-architecture

No summary provided by upstream source.

Repository SourceNeeds Review