.claude/skills/tauri-2/tauri-syntax/tauri-syntax-menu/SKILL.md
Use when creating application menus, system tray icons, context menus, or handling menu events in Tauri 2. Prevents using deprecated v1 menu patterns and missing menu event handler registration on the Builder. Covers MenuBuilder, menu item types, PredefinedMenuItem, context menus, TrayIconBuilder, and JavaScript Menu/TrayIcon APIs. Keywords: tauri menu, MenuBuilder, TrayIcon, system tray, context menu, PredefinedMenuItem, menu events.
npx skillsauth add OpenAEC-Foundation/OpenAEC-Workspace-Composer tauri-syntax-menuInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Type | Builder | Purpose |
|------|---------|---------|
| MenuItem | MenuItemBuilder | Basic clickable text item |
| CheckMenuItem | CheckMenuItemBuilder | Toggleable checkbox item |
| IconMenuItem | IconMenuItemBuilder | Item with icon |
| PredefinedMenuItem | -- | OS-native standard items |
| Submenu | SubmenuBuilder | Nested menu container |
Platform-standard items: About, Hide, HideOthers, ShowAll, CloseWindow, Quit, Copy, Cut, Paste, SelectAll, Undo, Redo, Minimize, Zoom, Separator, Fullscreen, Services, BringAllToFront.
| Method | Creates |
|--------|---------|
| .text(id, text) | MenuItem with ID and text |
| .separator() | Menu separator line |
| .quit() | PredefinedMenuItem::Quit |
| .undo() | PredefinedMenuItem::Undo |
| .redo() | PredefinedMenuItem::Redo |
| .cut() | PredefinedMenuItem::Cut |
| .copy() | PredefinedMenuItem::Copy |
| .paste() | PredefinedMenuItem::Paste |
| .select_all() | PredefinedMenuItem::SelectAll |
| Method | Signature | Description |
|--------|-----------|-------------|
| new() | () -> Self | Create builder |
| with_id(id) | (impl Into<TrayIconId>) -> Self | Set custom ID |
| icon(image) | (Image) -> Self | Set tray icon |
| tooltip(text) | (impl Into<String>) -> Self | Set tooltip |
| title(text) | (impl Into<String>) -> Self | Set title |
| menu(menu) | (&Menu) -> Self | Attach context menu |
| show_menu_on_left_click(bool) | (bool) -> Self | Show menu on left click (default: true) |
| icon_as_template(bool) | (bool) -> Self | macOS template icon |
| on_menu_event(F) | (F) -> Self | Menu click handler |
| on_tray_icon_event(F) | (F) -> Self | Tray icon click handler |
| build(manager) | (impl Manager) -> Result<TrayIcon> | Build tray icon |
| Class | Import | Description |
|-------|--------|-------------|
| Menu | @tauri-apps/api/menu | Menu container |
| MenuItem | @tauri-apps/api/menu | Clickable text item |
| Submenu | @tauri-apps/api/menu | Nested menu |
| CheckMenuItem | @tauri-apps/api/menu | Toggleable item |
| PredefinedMenuItem | @tauri-apps/api/menu | OS-standard item |
| TrayIcon | @tauri-apps/api/tray | System tray icon |
| Event Type | Description |
|------------|-------------|
| Click | Tray icon clicked |
| DoubleClick | Tray icon double-clicked |
| Enter | Cursor entered tray icon |
| Move | Cursor moved over tray icon |
| Leave | Cursor left tray icon |
Left, Right, Middle
NEVER call Builder::on_menu_event() in Tauri 2 -- it was removed. Use App::on_menu_event() or the builder .on_menu_event() method instead.
NEVER use Tauri v1 menu type names (CustomMenuItem, SystemTray, MenuItem for predefined items) -- they are renamed in v2.
ALWAYS call .build() on MenuBuilder and SubmenuBuilder -- forgetting .build() produces a builder, not a usable menu.
ALWAYS match menu event IDs as strings using event.id().as_ref() in Rust -- menu item IDs are compared as string references, not typed enums.
ALWAYS use Menu.new() (not new Menu()) in JavaScript -- menu items are created via async factory methods.
use tauri::menu::{MenuBuilder, SubmenuBuilder, PredefinedMenuItem};
tauri::Builder::default()
.menu(|app| {
let file_menu = SubmenuBuilder::new(app, "File")
.text("new", "New")
.text("open", "Open")
.separator()
.quit()
.build()?;
let edit_menu = SubmenuBuilder::new(app, "Edit")
.undo()
.redo()
.separator()
.cut()
.copy()
.paste()
.select_all()
.build()?;
MenuBuilder::new(app)
.item(&file_menu)
.item(&edit_menu)
.build()
})
.on_menu_event(|app, event| {
match event.id().as_ref() {
"new" => println!("New file"),
"open" => println!("Open file"),
_ => {}
}
})
.run(tauri::generate_context!())
.expect("error running app");
import { Menu, MenuItem, Submenu, PredefinedMenuItem, CheckMenuItem } from '@tauri-apps/api/menu';
const menu = await Menu.new({
items: [
await Submenu.new({
text: 'File',
items: [
await MenuItem.new({
text: 'Open',
accelerator: 'CmdOrCtrl+O',
action: () => { console.log('Open clicked'); },
}),
await MenuItem.new({
text: 'Save',
accelerator: 'CmdOrCtrl+S',
action: () => { console.log('Save clicked'); },
}),
await PredefinedMenuItem.new({ item: 'Separator' }),
await PredefinedMenuItem.new({ item: 'Quit' }),
],
}),
await Submenu.new({
text: 'View',
items: [
await CheckMenuItem.new({
text: 'Dark Mode',
checked: false,
action: (item) => { console.log('Toggled'); },
}),
],
}),
],
});
await menu.setAsAppMenu();
// Show context menu at cursor position
await menu.popup();
// Show context menu at specific position
await menu.popup({ x: 100, y: 200 });
use tauri::tray::TrayIconBuilder;
use tauri::menu::MenuBuilder;
use tauri::image::Image;
tauri::Builder::default()
.setup(|app| {
let menu = MenuBuilder::new(app)
.text("show", "Show Window")
.text("hide", "Hide Window")
.separator()
.text("quit", "Quit")
.build()?;
let _tray = TrayIconBuilder::new()
.icon(Image::from_path("icons/tray.png")?)
.tooltip("My Tauri App")
.menu(&menu)
.show_menu_on_left_click(true)
.on_menu_event(|app, event| {
match event.id().as_ref() {
"show" => {
if let Some(w) = app.get_webview_window("main") {
w.show().unwrap();
}
}
"quit" => app.exit(0),
_ => {}
}
})
.on_tray_icon_event(|tray, event| {
println!("Tray event: {:?}", event);
})
.build(app)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error running app");
import { TrayIcon } from '@tauri-apps/api/tray';
import { Menu, MenuItem } from '@tauri-apps/api/menu';
const menu = await Menu.new({
items: [
await MenuItem.new({ text: 'Show', action: () => showWindow() }),
await MenuItem.new({ text: 'Quit', action: () => exit(0) }),
],
});
const tray = await TrayIcon.new({
icon: 'icons/tray-icon.png',
tooltip: 'My App',
menu,
action: (event) => {
if (event.type === 'Click') {
console.log('Tray clicked with', event.button);
}
},
});
// Update at runtime
await tray.setTooltip('Updated tooltip');
await tray.setIcon('icons/new-icon.png');
await tray.setVisible(false);
// Dynamic menu manipulation
await menu.append(await MenuItem.new({ text: 'New Item', action: () => {} }));
await menu.prepend(await MenuItem.new({ text: 'First Item', action: () => {} }));
await menu.insert(1, await MenuItem.new({ text: 'At Index 1', action: () => {} }));
await menu.remove('item-id');
await menu.removeAt(0);
const item = await menu.get('item-id');
const allItems = await menu.items();
// On the builder
.on_menu_event(|app, event| {
match event.id().as_ref() {
"new" => println!("New file"),
"open" => println!("Open file"),
_ => {}
}
})
The event.id() returns a MenuId. Use .as_ref() to get a &str for pattern matching.
{
"app": {
"trayIcon": {
"iconPath": "icons/icon.png",
"iconAsTemplate": true
}
}
}
Menu operations require core:menu:default and tray operations require core:tray:default in the capability file.
{
"permissions": [
"core:menu:default",
"core:tray:default"
]
}
| Tauri v1 | Tauri v2 |
|----------|----------|
| Menu | MenuBuilder |
| CustomMenuItem | MenuItemBuilder |
| Submenu | SubmenuBuilder |
| MenuItem (predefined) | PredefinedMenuItem |
| SystemTray | TrayIconBuilder |
| SystemTrayEvent | TrayIconEvent |
development
Use when integrating Vite with a backend framework, rendering Vite assets from server-side templates, or setting up dev/production HTML serving. Prevents incorrect manifest.json traversal and missing CSS chunk resolution in production. Covers build.manifest configuration, .vite/manifest.json structure, ManifestChunk properties, dev mode HTML setup, production rendering, CSS/JS chunk resolution, and modulepreload polyfill. Keywords: backend integration, manifest.json, ManifestChunk, Django, Laravel, Rails, modulepreload.
development
Use when encountering dev server startup failures, HMR issues, proxy errors, CORS blocks, or module not found errors during development. Prevents misconfiguring server.hmr behind reverse proxies and forgetting appType: 'custom' in middleware mode. Covers HMR full-reload debugging, proxy configuration, CORS setup, HTTPS certificates, server.fs.strict violations, port conflicts, WebSocket failures, file watcher issues, and middleware mode. Keywords: dev server, HMR, proxy, CORS, HTTPS, WebSocket, port conflict, server.fs.strict, middleware mode, file watcher.
development
Use when encountering pre-bundling errors, dependency resolution failures, stale cache issues, or slow development server startup. Prevents excluding CJS dependencies from pre-bundling (which breaks runtime module resolution) and misconfiguring optimizeDeps. Covers CJS/ESM conversion failures, missing dependency auto-discovery, optimizeDeps configuration, monorepo linked dependencies, cache invalidation, browser cache staleness, and large dependency tree performance. Keywords: pre-bundling, optimizeDeps, CJS, ESM, cache, dependency resolution, monorepo, node_modules/.vite.
development
Use when encountering Vite build failures, chunk size warnings, or version-specific build errors. Prevents the common mistake of using deprecated rollupOptions in v8 or misconfiguring build targets and minifiers. Covers Rolldown/Rollup bundling failures, CSS minification errors, sourcemap problems, library mode build failures, BundleError handling, and asset processing errors. Keywords: build error, Rolldown, chunk size, sourcemap, library mode, minify, BundleError, rollupOptions, build.target.