Back to Blog

Add System Tray to Tauri App | Show/Hide/Quit Menu Tutorial

Sandy LaneSandy Lane

Video: Add System Tray to Tauri App | Show/Hide/Quit Menu Tutorial by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

An icon in the OS menubar with a Show / Hide / Quit menu. The Tauri 2 way using TrayIconBuilder.

A system tray icon — that small icon in the macOS menubar, the Windows system tray, or the Linux notification area — turns your app into a background utility. Users can hide the window and bring it back via the tray. Some apps live entirely in the tray with no main window at all.

This lesson uses Tauri 2's TrayIconBuilder (the v1 SystemTray API has been replaced).

Enable the feature

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

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

Without this feature, the tray API isn't available.

Build the tray icon

In src-tauri/src/lib.rs:

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

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

      let _tray = TrayIconBuilder::with_id("main-tray")
        .icon(app.default_window_icon().unwrap().clone())
        .menu(&menu)
        .on_menu_event(|app, event| {
          let window = app.get_webview_window("main").unwrap();
          match event.id().as_ref() {
            "show" => { window.show().ok(); window.set_focus().ok(); }
            "hide" => { window.hide().ok(); }
            "quit" => { app.exit(0); }
            _ => {}
          }
        })
        .build(app)?;

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

Five steps:

  1. Build menu items (MenuItem::with_id) with stable IDs ("show", "hide", "quit") and labels.
  2. Combine them into a Menu.
  3. Configure the tray with TrayIconBuilder::with_id, attaching the icon and menu.
  4. Provide on_menu_event to handle clicks.
  5. Call .build(app) to install the tray.

The setup closure runs once when the app starts. The tray persists for the app's lifetime.

The icon

.icon(app.default_window_icon().unwrap().clone())

We reuse the app's window icon. For a different tray icon, load an image:

use tauri::image::Image;

let icon = Image::from_bytes(include_bytes!("../icons/tray-icon.png"))?;
TrayIconBuilder::new().icon(icon).build(app)?;

For best appearance on macOS, use a template icon — black-only with transparency. macOS auto-tints it for light/dark mode.

Hide on close instead of quit

A common pattern: when the user clicks the close button, hide the window instead of quitting. Continue running in the tray. The tray menu then provides the actual quit.

.on_window_event(|window, event| {
  if let tauri::WindowEvent::CloseRequested { api, .. } = event {
    window.hide().ok();
    api.prevent_close();
  }
})

Add this to Builder::default() (between setup and run). Now closing the window hides it; "Quit" from the tray menu actually exits.

Click vs menu

on_menu_event fires when the user clicks a menu item. For clicks on the tray icon itself (without opening the menu), use on_tray_icon_event:

.on_tray_icon_event(|tray, event| {
  if let TrayIconEvent::Click { button: tauri::tray::MouseButton::Left, .. } = event {
    let app = tray.app_handle();
    if let Some(window) = app.get_webview_window("main") {
      let _ = window.show();
      let _ = window.set_focus();
    }
  }
})

Left-click toggles the window. Right-click opens the menu (default behaviour). Double-click and middle-click are also detectable.

Dynamic menu items

For menus that change based on app state (recent files, online users, etc.), build a fresh menu and call set_menu on the tray:

fn rebuild_menu(app: &AppHandle) -> tauri::Result<Menu<tauri::Wry>> {
  // build menu items based on current state
}

let new_menu = rebuild_menu(app)?;
let tray = app.tray_by_id("main-tray").unwrap();
tray.set_menu(Some(new_menu))?;

Trigger a rebuild when the underlying state changes — e.g., from a tauri::command that writes to state.

Tooltips

TrayIconBuilder::new()
  .icon(...)
  .tooltip("My App")
  .build(app)?;

A tooltip shows when the user hovers the tray icon.

Notifications

The tray plays well with desktop notifications (next lesson). A common pattern: the tray icon is the app's home; notifications surface specific events; clicking a notification brings the main window forward.

Common mistakes

Forgetting the tray-icon feature. Compiles without it, but TrayIconBuilder isn't available. The compiler error points at the unresolved import.

Using v1 names. SystemTray, SystemTrayMenu, app.tray_handle() — these are Tauri 1 APIs. Tauri 2 uses TrayIconBuilder and Menu (from tauri::menu).

Quitting from "close" by accident. If you don't prevent_close, the close button quits the app. Window-event handler is required for hide-on-close.

Tray icon too colorful on macOS. macOS auto-tints template icons but expects black-only with transparency. Color icons may look bad against dark menubars.

Menu items with the same ID. The match event.id() would fire for the wrong handler. Use unique IDs per item.

What's next

Next lesson: desktop notifications. Pairs naturally with the tray icon. Send notifications from Rust; respond when the user clicks them.

Recap

TrayIconBuilder::with_id to create the tray. Build a Menu of MenuItem::with_id items. Handle clicks in on_menu_event (matching by ID) and tray icon clicks in on_tray_icon_event. Add the tray-icon feature to tauri in Cargo.toml. Use prevent_close on the window's close event for hide-on-close.

Next: desktop notifications.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.