How to Implement System Tray Icons in Tauri Applications: Complete Guide with Examples

Tauri provides a unified cross-platform API for implementing system tray icons through the @tauri-apps/api/tray JavaScript module and the TrayIconBuilder Rust struct, enabling both frontend and backend control of tray icons, menus, and click events.

Tauri enables developers to implement system tray icons that work consistently across Windows, macOS, and Linux through a dual-layer architecture. The system tray implementation in tauri-apps/tauri allows you to create, manage, and respond to tray icon events from both JavaScript/TypeScript and Rust, with automatic resource management handled by the TrayManager.

Understanding the Tauri System Tray Architecture

The implementation consists of three distinct layers working together to abstract platform-specific quirks:

  • Frontend API Layer: Located in packages/api/src/tray.ts, this exports TrayIcon, TrayIconOptions, and event types. It marshals calls to Rust via Tauri's command channel and forwards events back to JavaScript through a registered Channel.

  • Rust Builder Layer: The TrayIconBuilder<R> in crates/tauri/src/tray/mod.rs constructs tray icons, registers them with the global TrayManager, and stores the native tray_icon::TrayIcon object while attaching optional menu and icon event listeners.

  • Manager/Runtime Layer: The TrayManager<R> in crates/tauri/src/manager/tray.rs maintains a map of tray icon IDs to resource IDs, forwards global events, and handles removal when the last handle is dropped or when remove_by_id is called.

Implementing System Tray Icons from the Frontend

Creating a Tray Icon with JavaScript/TypeScript

To implement a system tray icon from the frontend, import TrayIcon from @tauri-apps/api/tray and optionally Menu from @tauri-apps/api/menu to attach a context menu. The TrayIconOptions interface (defined at lines 74-138 of packages/api/src/tray.ts) supports icons as ArrayBuffer or path strings, tooltips, titles, and event callbacks.

import { TrayIcon, type TrayIconOptions } from '@tauri-apps/api/tray';
import { Menu, Submenu } from '@tauri-apps/api/menu';

// Build a menu (optional)
const menu = await Menu.new();
await menu.append(new Submenu('File', await Menu.new()));

// Define tray options
const trayOpts: TrayIconOptions = {
  id: 'my-tray',
  tooltip: 'My Tauri App',
  icon: await fetch('/icons/tray.png').then(r => r.arrayBuffer()),
  menu,
  action: (event) => {
    if (event.type === 'Click' && event.button === 'Left') {
      console.log('Tray icon left-clicked');
    }
  }
};

// Create the tray icon (lines 92-108 of tray.ts)
const tray = await TrayIcon.new(trayOpts);

// Dynamic updates
await tray.setTooltip('Updated tooltip');
await tray.setTitle('My Title');  // macOS only
await tray.setVisible(false);

Handling Tray Events in JavaScript

The action callback receives TrayIconEvent objects when users interact with the icon. Events include click types, button states (left/right), and hover states. The frontend receives these through the Rust-to-JavaScript channel established during TrayIcon.new().

Implementing System Tray Icons from the Backend

Using TrayIconBuilder in Rust

For backend implementation, use TrayIconBuilder::with_id() (lines 24-33 of crates/tauri/src/tray/mod.rs) to construct tray icons in your setup closure or any function receiving &AppHandle<R>. The builder pattern supports method chaining for configuration.

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

pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
    // Build menu
    let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
    let menu = Menu::with_items(app, &[&quit])?;

    // Construct tray (lines 36-44 for event registration, 85-89 for build)
    let tray = TrayIconBuilder::with_id("my-tray")
        .icon(app.default_window_icon().unwrap().clone())
        .tooltip("My Tauri App")
        .menu(&menu)
        .on_tray_icon_event(|tray, event| {
            if let TrayIconEvent::Click {
                button: MouseButton::Left,
                button_state: MouseButtonState::Up,
                ..
            } = event
            {
                // Restore main window on click
                if let Some(window) = tray.app_handle().get_webview_window("main") {
                    let _ = window.unminimize();
                    let _ = window.show();
                    let _ = window.set_focus();
                }
            }
        })
        .build(app)?;
    
    Ok(())
}

Dynamic Menu Switching and Advanced Patterns

The official example in examples/api/src-tauri/src/tray.rs demonstrates advanced patterns including dynamic menu swapping and state management using AtomicBool:

let menu1 = Menu::with_items(app, &[/* items */])?;
let menu2 = Menu::with_items(app, &[/* items */])?;
let is_menu1 = AtomicBool::new(true);

TrayIconBuilder::with_id("tray-1")
    .icon(app.default_window_icon().unwrap().clone())
    .menu(&menu1)
    .show_menu_on_left_click(false)
    .on_menu_event(move |app, event| {
        match event.id.as_ref() {
            "switch-menu" => {
                let flag = is_menu1.load(Ordering::Relaxed);
                let (new_menu, tooltip) = if flag { 
                    (menu2.clone(), "Menu 2") 
                } else { 
                    (menu1.clone(), "Tauri") 
                };
                
                if let Some(tray) = app.tray_by_id("tray-1") {
                    let _ = tray.set_menu(Some(new_menu));
                    let _ = tray.set_tooltip(Some(tooltip));
                }
                is_menu1.store(!flag, Ordering::Relaxed);
            }
            _ => {}
        }
    })
    .build(app)?;

Platform-Specific Considerations

When implementing system tray icons in Tauri, account for these platform differences through builder methods:

  • Linux: Requires an attached menu for visibility in many desktop environments. Use show_menu_on_left_click(true) to ensure accessibility.
  • macOS: Supports template icons (icon_as_template) for automatic color adaptation to light/dark mode and title text alongside icons using set_title().
  • Windows: Supports tooltip text and standard icon formats, with temp_dir_path available for icon resource management.

Summary

  • Tauri provides both frontend (@tauri-apps/api/tray) and backend (TrayIconBuilder) APIs for implementing system tray icons with identical capabilities.
  • The TrayManager in crates/tauri/src/manager/tray.rs (lines 48-84) automatically handles resource cleanup when tray icon handles are dropped.
  • Menu events flow through GlobalMenuEventListener while tray icon events use GlobalTrayIconEventListener, both marshaled to the frontend as TrayIconEvent objects.
  • Platform-specific behaviors are abstracted through builder methods like show_menu_on_left_click, icon_as_template, and set_title.

Frequently Asked Questions

Can I update the tray icon dynamically after creation?

Yes. Both the JavaScript TrayIcon instance and the Rust TrayIcon type provide methods like set_icon(), set_tooltip(), and set_menu() for runtime updates. In JavaScript, call await tray.setIcon(newIconBuffer); in Rust, access the tray via app.tray_by_id("id") and call set_menu() or set_tooltip() as shown in examples/api/src-tauri/src/tray.rs.

How do I handle left-click versus right-click events?

In Rust, pattern match on TrayIconEvent::Click and check the button field against MouseButton::Left or MouseButton::Right. In JavaScript, check event.button in the action callback. Note that macOS and Linux may require show_menu_on_left_click(false) to prevent the default menu from intercepting left-clicks.

Do I need to manually clean up tray icons when the app closes?

No. The TrayManager automatically removes native tray icons when the last TrayIcon handle is dropped or when the application exits. However, you can explicitly call TrayIcon::close() in JavaScript or TrayIcon::remove_by_id() in Rust if you need to destroy the icon while the app continues running.

Can I create multiple tray icons in a single Tauri application?

Yes. Each tray icon requires a unique ID passed to TrayIconBuilder::with_id() or the id field in TrayIconOptions. The TrayManager stores these in a HashMap (lines 48-84 of crates/tauri/src/manager/tray.rs), allowing you to retrieve specific icons later via app.tray_by_id() for individual updates or removal.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →