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 exportsTrayIcon,TrayIconOptions, and event types. It marshals calls to Rust via Tauri's command channel and forwards events back to JavaScript through a registeredChannel. -
Rust Builder Layer: The
TrayIconBuilder<R>incrates/tauri/src/tray/mod.rsconstructs tray icons, registers them with the globalTrayManager, and stores the nativetray_icon::TrayIconobject while attaching optional menu and icon event listeners. -
Manager/Runtime Layer: The
TrayManager<R>incrates/tauri/src/manager/tray.rsmaintains a map of tray icon IDs to resource IDs, forwards global events, and handles removal when the last handle is dropped or whenremove_by_idis 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 usingset_title(). - Windows: Supports tooltip text and standard icon formats, with
temp_dir_pathavailable 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
TrayManagerincrates/tauri/src/manager/tray.rs(lines 48-84) automatically handles resource cleanup when tray icon handles are dropped. - Menu events flow through
GlobalMenuEventListenerwhile tray icon events useGlobalTrayIconEventListener, both marshaled to the frontend asTrayIconEventobjects. - Platform-specific behaviors are abstracted through builder methods like
show_menu_on_left_click,icon_as_template, andset_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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →