tauri-tray-menu
Use when building a Tauri v2 tray icon, system menu bar (macOS), window menu, or context menus — including click handlers, dynamic menu updates, and positioning a window relative to the tray.
Tauri v2 — Tray, Menus, and Menu-Bar Windows
Covers the three menu surfaces in Tauri v2:
- Tray icon —
tauri::tray::TrayIconBuilder(status-bar/notification-area icon). - App/window menu —
app.set_menu(...)(macOS menu bar; Windows/Linux per-window menu). - Context menus —
menu.popup(window)triggered from JS.
Plus the menu-bar-app pattern (tray icon + popover window anchored under it via
tauri-plugin-positioner).
1. Cargo features
Tray support is gated behind a feature flag. Without it, tauri::tray::* won’t compile.
# src-tauri/Cargo.toml
[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
# Add "image-png" if you embed PNG menu icons via Image::from_bytes
For tray-relative window positioning:
tauri-plugin-positioner = { version = "2", features = ["tray-icon"] }
npm install @tauri-apps/plugin-positioner # only if you call moveWindow from JS
2. Build a tray icon
All tray construction happens inside .setup(|app| { ... }). The returned TrayIcon
can be dropped — Tauri keeps it alive internally — but keep the handle if you need
to update the icon/menu later.
use tauri::{
menu::{Menu, MenuItem},
tray::TrayIconBuilder,
};
tauri::Builder::default()
.setup(|app| {
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&quit_i])?;
let _tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.show_menu_on_left_click(false) // we want left-click to toggle the window
.on_menu_event(|app, event| match event.id.as_ref() {
"quit" => app.exit(0),
_ => {}
})
.on_tray_icon_event(|tray, event| {
// see §5 for click pattern matching
})
.build(app)?;
Ok(())
});
Notes:
TrayIconBuilder::with_id("my-tray")if you’ll look it up later viaapp.tray_by_id.show_menu_on_left_click(true)is the default — disable it for menu-bar popover apps so left-click can toggle a window instead.
3. Menus — items, predefined, submenus, icons, checks
tauri::menu has two flavours: low-level constructors (Menu::with_items,
MenuItem::with_id) and ergonomic builders (MenuBuilder, SubmenuBuilder).
Mix freely.
Items
| Type | Use |
|---|---|
MenuItem | plain text item with an id |
PredefinedMenuItem | OS-standard items (copy, paste, quit, separator, select_all, undo, redo, about, services, hide, close_window, …) |
CheckMenuItem | toggle with checkmark |
IconMenuItem | item with a leading icon |
Submenu | nests items |
Builder example (typical app menu)
use tauri::menu::{MenuBuilder, SubmenuBuilder, PredefinedMenuItem};
let edit = SubmenuBuilder::new(app, "Edit")
.undo().redo().separator()
.cut().copy().paste().select_all()
.build()?;
let menu = MenuBuilder::new(app)
.item(&edit)
.text("open", "Open…")
.check("auto_update", "Check for updates")
.separator()
.item(&PredefinedMenuItem::quit(app, None)?)
.build()?;
app.set_menu(menu)?;
app.set_menu(menu) puts the menu in the macOS menu bar (system-wide for the app)
or, on Windows/Linux, into the window chrome. Per-window menus: window.set_menu(...).
Handling events
app.on_menu_event(|app, event| match event.id().0.as_str() {
"open" => { /* … */ }
"auto_update" => { /* … */ }
_ => {}
});
When attached to a tray, use TrayIconBuilder::on_menu_event instead — it fires for
clicks on tray menu items specifically.
4. Dynamic updates
Hold MenuItem / CheckMenuItem / IconMenuItem handles in your setup closure
(or in app state) and mutate them in place:
let status = MenuItem::with_id(app, "status", "Status: idle", false, None::<&str>)?;
// ... later, from anywhere with a clone of `status` ...
status.set_text("Status: running")?;
status.set_enabled(true)?;
Other setters: set_checked, set_icon, set_accelerator, set_native_icon.
To look an item up after the fact:
menu.get("status").unwrap().as_menuitem_unchecked().set_text("Status: ready")?;
For long-lived state, stash handles in app.manage(MyMenuState { status, ... })
and pull them out in commands via tauri::State<MyMenuState>.
5. Tray click events — left vs right
TrayIconEvent variants: Click { button, button_state, position, rect, .. },
DoubleClick, Enter, Move, Leave. Buttons: MouseButton::{Left,Right,Middle}.
States: MouseButtonState::{Up,Down}.
Match on button_state: Up to fire on release (matches OS conventions).
use tauri::{
Manager,
tray::{MouseButton, MouseButtonState, TrayIconEvent},
};
.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(win) = app.get_webview_window("main") {
let _ = win.show();
let _ = win.set_focus();
}
}
_ => {}
})
On macOS, right-click is intercepted automatically when a menu is attached and
show_menu_on_left_click(false) is set — you don’t need to handle it manually.
6. Context menus from JS
To pop a menu from a frontend click (e.g. on right-click of an element):
#[tauri::command]
fn show_context_menu(window: tauri::WebviewWindow) -> tauri::Result<()> {
let menu = tauri::menu::MenuBuilder::new(&window)
.text("rename", "Rename")
.text("delete", "Delete")
.build()?;
menu.popup(window.clone())?;
Ok(())
}
import { invoke } from '@tauri-apps/api/core';
element.addEventListener('contextmenu', (e) => {
e.preventDefault();
invoke('show_context_menu');
});
popup(window) shows it at the cursor; popup_at(window, position) for a specific point.
7. Menu-bar app — anchor a window to the tray
This is the killer pattern for macOS-style menu-bar apps (popover under the tray
icon). The tauri-plugin-positioner plugin computes tray coordinates so you don’t
have to.
Capabilities
src-tauri/capabilities/default.json:
{ "permissions": ["positioner:default"] }
Wire it in setup
use tauri_plugin_positioner::{Position, WindowExt};
app.handle().plugin(tauri_plugin_positioner::init());
let tray = TrayIconBuilder::new()
.on_tray_icon_event(|tray, event| {
// Plugin uses this to remember the last-known tray rect.
tauri_plugin_positioner::on_tray_event(tray.app_handle(), &event);
})
.build(app)?;
Move the window on click
TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } => {
let app = tray.app_handle();
if let Some(win) = app.get_webview_window("popover") {
let _ = win.move_window(Position::TrayCenter); // or TrayLeft, TrayRight, …
let _ = win.show();
let _ = win.set_focus();
}
}
Useful Position variants: TrayLeft, TrayCenter, TrayRight, TrayBottomLeft,
TrayBottomCenter, TrayBottomRight, plus screen-relative TopRight, BottomLeft, etc.
In tauri.conf.json, configure the popover window:
{ "label": "popover", "decorations": false, "transparent": true,
"alwaysOnTop": true, "skipTaskbar": true, "visible": false,
"width": 360, "height": 480 }
Hide on blur so it dismisses like a real menu-bar popover (listen for
WindowEvent::Focused(false)).
8. Templates
templates/tray.rs— full menu-bar-app pattern: tray icon + popover toggle on left-click + Quit menu item + positioner.templates/capabilities-tray.json— the positioner permission stanza.
Quick checklist
tray-iconfeature in Cargo.toml.- Tray built inside
.setup(...). show_menu_on_left_click(false)if left-click should toggle a window.- Use
button_state: MouseButtonState::Upin click matches. - Stash mutable
MenuItemhandles forset_text/set_enabledlater. - For tray-relative window: install
tauri-plugin-positionerwith thetray-iconfeature, addpositioner:defaultpermission, and calltauri_plugin_positioner::on_tray_eventfromon_tray_icon_event.