Skip to content
Blog

31 October 2026 · 1 min read

How to build a Windows tray app with Tauri 2 (step by step)

TL;DR

Goal: build a small Windows app that lives in the system tray, responds to a global hotkey, starts with Windows and ships as an installer, using Tauri 2.

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. 1

    Rust (stable)

    Install with rustup; MSVC toolchain on Windows.

  2. 2

    Microsoft C++ Build Tools

    "Desktop development with C++" workload.

  3. 3

    WebView2

    Already on Windows 11 and up-to-date Windows 10.

  4. 4

    Node.js LTS

    For the front end and the Tauri CLI.

DEEP DIVECheck your setup
rustc --version
cargo --version
node --version

03Create the project

npm create tauri-app@latest my-tray-app
cd my-tray-app
npm install
npm run tauri dev

Pick 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-shortcut

Register 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 autostart
import { 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 build

Installers land in src-tauri/target/release/bundle/ (nsis/ and msi/).

NSIS (.exe)MSI
Per-user installYes, no admin neededUsually machine-wide
Enterprise / GPO deployPossibleStandard choice
Works with the updater pluginYesYes

08Tauri 2 or Electron?

Tauri 2Electron
RenderingSystem WebView (WebView2)Bundled Chromium
Back endRustNode.js
Installer sizeSmaller (no browser shipped)Larger
Native API accessExplicit, permission-basedBroad by default
EcosystemGrowing plugins and cratesVery mature npm ecosystem

09Troubleshooting

  1. "not allowed" errors in JS

    Add the plugin permission to capabilities/default.json.

  2. Hotkey does nothing

    Another app may own that combo, so register() fails. Pick another.

  3. No tray icon

    Check the tray-icon feature and that .icon(...) is set.

  4. SmartScreen warning

    Unsigned installers get flagged. Sign with a code-signing certificate.

10Checklist

  1. Runs in dev

    npm run tauri dev opens a window.

  2. Tray works

    Show and Quit respond; close hides to tray.

  3. Hotkey works

    Even with the window hidden.

  4. Installer tested

    On a clean Windows machine.