31 October 2026 · 1 min read
How to build a Windows tray app with Tauri 2 (step by step)
TL;DR
Tray icon + menu
Show, hide and quit from the tray.
Global hotkey
Works even when the app is hidden.
Start with Windows
Via the autostart plugin.
Installer
NSIS or MSI from one command.
01What you'll build
Scaffold
create-tauri-app
Tray
tray-icon feature
Hotkey
global-shortcut
Autostart
plugin
Build
tauri build
02Prerequisites
- 1
Rust (stable)
Install with rustup; MSVC toolchain on Windows.
- 2
Microsoft C++ Build Tools
"Desktop development with C++" workload.
- 3
WebView2
Already on Windows 11 and up-to-date Windows 10.
- 4
Node.js LTS
For the front end and the Tauri CLI.
DEEP DIVECheck your setup
rustc --version
cargo --version
node --version03Create the project
npm create tauri-app@latest my-tray-app
cd my-tray-app
npm install
npm run tauri devPick any front-end template (vanilla, React, Vue...). A window should open.
04Add a tray icon and menu
Enable the tray feature in src-tauri/Cargo.toml:
tauri = { version = "2", features = ["tray-icon"] }Build the tray in src-tauri/src/lib.rs:
use tauri::{
menu::{Menu, MenuItem},
tray::TrayIconBuilder,
Manager,
};
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let show = MenuItem::with_id(app, "show", "Show", true, None::<&str>)?;
let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show, &quit])?;
TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.on_menu_event(|app, event| match event.id.as_ref() {
"show" => {
if let Some(w) = app.get_webview_window("main") {
let _ = w.show();
let _ = w.set_focus();
}
}
"quit" => app.exit(0),
_ => {}
})
.build(app)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}DEEP DIVEHide to tray instead of quitting when the window closes
// on the Builder, before .run(...)
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
let _ = window.hide();
api.prevent_close();
}
})05Register a global hotkey
npm run tauri add global-shortcutRegister it from Rust inside setup:
#[cfg(desktop)]
{
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState};
let hotkey = Shortcut::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::Space);
app.handle().plugin(
tauri_plugin_global_shortcut::Builder::new()
.with_handler(move |app, shortcut, event| {
if shortcut == &hotkey && event.state() == ShortcutState::Pressed {
if let Some(w) = app.get_webview_window("main") {
let _ = w.show();
let _ = w.set_focus();
}
}
})
.build(),
)?;
app.global_shortcut().register(hotkey)?;
}DEEP DIVERegistering from JavaScript instead
Plugin commands are blocked by default, so allow them in src-tauri/capabilities/default.json:
"permissions": [
"core:default",
"global-shortcut:allow-register",
"global-shortcut:allow-unregister"
]import { register } from '@tauri-apps/plugin-global-shortcut';
await register('CommandOrControl+Shift+Space', (e) => {
if (e.state === 'Pressed') console.log('hotkey pressed');
});06Start with Windows
npm run tauri add autostartimport { enable, isEnabled } from '@tauri-apps/plugin-autostart';
if (!(await isEnabled())) await enable();Add autostart:allow-enable and autostart:allow-is-enabled to your capability. Put this behind a settings toggle rather than forcing it on.
07Build the installer
npm run tauri buildInstallers land in src-tauri/target/release/bundle/ (nsis/ and msi/).
| NSIS (.exe) | MSI | |
|---|---|---|
| Per-user install | Yes, no admin needed | Usually machine-wide |
| Enterprise / GPO deploy | Possible | Standard choice |
| Works with the updater plugin | Yes | Yes |
08Tauri 2 or Electron?
| Tauri 2 | Electron | |
|---|---|---|
| Rendering | System WebView (WebView2) | Bundled Chromium |
| Back end | Rust | Node.js |
| Installer size | Smaller (no browser shipped) | Larger |
| Native API access | Explicit, permission-based | Broad by default |
| Ecosystem | Growing plugins and crates | Very mature npm ecosystem |
09Troubleshooting
"not allowed" errors in JS
Add the plugin permission to capabilities/default.json.
Hotkey does nothing
Another app may own that combo, so register() fails. Pick another.
No tray icon
Check the tray-icon feature and that .icon(...) is set.
SmartScreen warning
Unsigned installers get flagged. Sign with a code-signing certificate.
10Checklist
Runs in dev
npm run tauri dev opens a window.
Tray works
Show and Quit respond; close hides to tray.
Hotkey works
Even with the window hidden.
Installer tested
On a clean Windows machine.