All tests run on an 8-year-old MacBook Air.
A menubar app looks simple from the outside. Click the icon, panel appears, click away, panel hides.
The implementation has more moving parts than you'd expect — especially in Tauri v2, where menubar support changed significantly from v1.
The basic setup
Tauri v2 doesn't have a built-in "menubar mode." You build it by combining a few primitives: a system tray icon, a frameless window, and some positioning logic.
use tauri::{
Manager, SystemTray, SystemTrayEvent,
WindowBuilder, WindowUrl,
};
fn main() {
let tray = SystemTray::new();
tauri::Builder::default()
.system_tray(tray)
.on_system_tray_event(|app, event| {
if let SystemTrayEvent::LeftClick { position, size, .. } = event {
let window = app.get_window("main").unwrap();
position_panel_at_tray(&window, position, size);
window.show().unwrap();
window.set_focus().unwrap();
}
})
.run(tauri::generate_context!())
.unwrap();
}
Positioning the panel under the tray icon
The tray icon position comes from the click event. The panel needs to appear directly below it, centered horizontally.
fn position_panel_at_tray(
window: &tauri::Window,
tray_pos: tauri::PhysicalPosition,
tray_size: tauri::PhysicalSize,
) {
let panel_size = window.outer_size().unwrap();
let x = tray_pos.x + (tray_size.width / 2.0) - (panel_size.width as f64 / 2.0);
let y = tray_pos.y + tray_size.height;
window.set_position(tauri::PhysicalPosition::new(x, y)).unwrap();
}
On multi-monitor setups this gets complicated — the tray position is in global screen coordinates, and you need to account for which display the tray is on.
Hiding when focus is lost
Click outside the panel → panel disappears. This is the expected behavior that's surprisingly annoying to implement.
// In your React frontend
useEffect(() => {
const handleBlur = () => {
appWindow.hide();
};
window.addEventListener('blur', handleBlur);
return () => window.removeEventListener('blur', handleBlur);
}, []);
One catch: if the user clicks a native dialog (like a file picker) launched from your panel, the window loses focus and hides before the dialog appears. You need to suppress blur-hiding during native dialogs.
Keeping it out of the Dock and App Switcher
A menubar app shouldn't appear in the Dock or Cmd+Tab switcher.
// tauri.conf.json
{
"tauri": {
"macOS": {
"activationPolicy": "accessory"
}
}
}
accessory policy: no Dock icon, no App Switcher entry, stays in menubar only.
Launch at login
Users expect menubar apps to start automatically. Tauri doesn't handle this — you register a LaunchAgent manually:
use std::fs;
pub fn enable_launch_at_login(app_path: &str) {
let plist = format!(r#"
Label
com.hiyoko.app
ProgramArguments
{}
RunAtLoad
"#, app_path);
let plist_path = dirs::home_dir()
.unwrap()
.join("Library/LaunchAgents/com.hiyoko.app.plist");
fs::write(plist_path, plist).unwrap();
}
Hiyoko PDF Vault → https://hiyokoko.gumroad.com/l/HiyokoPDFVault
X → @hiyoyok
Top comments (0)