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

> Learn to implement system tray icons in Tauri apps with our complete guide. Control icons menus and clicks seamlessly across platforms using the @tauri-apps/api/tray module and TrayIconBuilder.

- Repository: [Tauri/tauri](https://github.com/tauri-apps/tauri)
- Tags: how-to-guide
- Published: 2026-02-26

---

**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`](https://github.com/tauri-apps/tauri/blob/main/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`](https://github.com/tauri-apps/tauri/blob/main/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`](https://github.com/tauri-apps/tauri/blob/main/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`](https://github.com/tauri-apps/tauri/blob/main/packages/api/src/tray.ts)) supports icons as `ArrayBuffer` or path strings, tooltips, titles, and event callbacks.

```typescript
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`](https://github.com/tauri-apps/tauri/blob/main/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.

```rust
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`](https://github.com/tauri-apps/tauri/blob/main/examples/api/src-tauri/src/tray.rs) demonstrates advanced patterns including dynamic menu swapping and state management using `AtomicBool`:

```rust
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`](https://github.com/tauri-apps/tauri/blob/main/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`](https://github.com/tauri-apps/tauri/blob/main/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`](https://github.com/tauri-apps/tauri/blob/main/crates/tauri/src/manager/tray.rs)), allowing you to retrieve specific icons later via `app.tray_by_id()` for individual updates or removal.