docs/src/content/docs/features/menus/application.mdx
import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components"; import { Image } from "astro:assets"; import applicationMenuMacOS from "../../../../assets/screenshots/application-menu-macos.png";
Professional desktop applications need menu bars—File, Edit, View, Help. But menus work differently on each platform:
Building platform-appropriate menus manually is tedious and error-prone.
Wails provides a unified API that creates platform-native menus automatically. Write once, get native behaviour on all platforms.
<Image src={applicationMenuMacOS} alt="A Wails application menu on macOS with standard, checkbox, radio, and submenu items" style="max-width: 280px; margin: 1.5rem auto; display: block; border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.2);" />On macOS, the application menu is placed in the global menu bar. This capture shows the native menu rendered by the Wails menu API, including disabled, checkbox, radio, and submenu items.
package main
import (
"runtime"
"github.com/wailsapp/wails/v3/pkg/application"
)
func main() {
app := application.New(application.Options{
Name: "My App",
})
// Create menu
menu := app.NewMenu()
// Add standard menus (platform-appropriate)
if runtime.GOOS == "darwin" {
menu.AddRole(application.AppMenu) // macOS only
}
menu.AddRole(application.FileMenu)
menu.AddRole(application.EditMenu)
menu.AddRole(application.WindowMenu)
menu.AddRole(application.HelpMenu)
// Set the application menu
app.Menu.Set(menu)
// Create window with UseApplicationMenu to inherit the menu on Windows/Linux
app.Window.NewWithOptions(application.WebviewWindowOptions{
UseApplicationMenu: true,
})
app.Run()
}
That's it! You now have platform-native menus with standard items. The UseApplicationMenu option ensures Windows and Linux windows display the menu without additional code.
// Create a new menu
menu := app.NewMenu()
// Add a top-level menu
fileMenu := menu.AddSubmenu("File")
// Add menu items
fileMenu.Add("New").OnClick(func(ctx *application.Context) {
// Handle New
})
fileMenu.Add("Open").OnClick(func(ctx *application.Context) {
// Handle Open
})
fileMenu.AddSeparator()
fileMenu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
})
Recommended approach — Use UseApplicationMenu for cross-platform consistency:
// Set the application menu once
app.Menu.Set(menu)
// Create windows that inherit the menu on Windows/Linux
app.Window.NewWithOptions(application.WebviewWindowOptions{
UseApplicationMenu: true, // Window uses the app menu
})
This approach:
UseApplicationMenu: true displays the app menuPlatform-specific details:
<Tabs syncKey="platform"> <TabItem label="macOS" icon="apple"> **Global menu bar** (one per application):```go
app.Menu.Set(menu)
```
The menu appears at the top of the screen and persists even when all windows are closed. The `UseApplicationMenu` option has no effect on macOS since all apps use the global menu.
```go
// Option 1: Use application menu (recommended)
app.Menu.Set(menu)
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
UseApplicationMenu: true,
})
// Option 2: Set menu directly on window
window.SetMenu(menu)
```
Each window can have its own menu, or inherit the application menu. The menu appears in the window's title bar.
```go
// Option 1: Use application menu (recommended)
app.Menu.Set(menu)
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
UseApplicationMenu: true,
})
// Option 2: Set menu directly on window
window.SetMenu(menu)
```
Behaviour varies by desktop environment. Some (like Unity) support global menus.
:::tip[Simplify Cross-Platform Menus]
Using UseApplicationMenu: true eliminates the need for platform-specific code like:
// Old approach - no longer needed
if runtime.GOOS == "darwin" {
app.Menu.Set(menu)
} else {
window.SetMenu(menu)
}
:::
Per-window custom menus:
If a window needs a different menu than the application menu, set it directly:
window.SetMenu(customMenu) // Overrides UseApplicationMenu
Wails provides predefined menu roles that create platform-appropriate menu structures automatically.
| Role | Description | Platform Notes |
|---|---|---|
AppMenu | Application menu with About, Preferences, Quit | macOS only |
FileMenu | File operations (New, Open, Save, etc.) | All platforms |
EditMenu | Text editing (Undo, Redo, Cut, Copy, Paste) | All platforms |
WindowMenu | Window management (Minimise, Zoom, etc.) | All platforms |
HelpMenu | Help and information | All platforms |
menu := app.NewMenu()
// macOS: Add application menu
if runtime.GOOS == "darwin" {
menu.AddRole(application.AppMenu)
}
// All platforms: Add standard menus
menu.AddRole(application.FileMenu)
menu.AddRole(application.EditMenu)
menu.AddRole(application.WindowMenu)
menu.AddRole(application.HelpMenu)
What you get:
<Tabs syncKey="platform"> <TabItem label="macOS" icon="apple"> **AppMenu** (with app name): - About [App Name] - Preferences... (⌘,) - --- - Services - --- - Hide [App Name] (⌘H) - Hide Others (⌥⌘H) - Show All - --- - Quit [App Name] (⌘Q)**FileMenu**:
- New (⌘N)
- Open... (⌘O)
- ---
- Close Window (⌘W)
**EditMenu**:
- Undo (⌘Z)
- Redo (⇧⌘Z)
- ---
- Cut (⌘X)
- Copy (⌘C)
- Paste (⌘V)
- Select All (⌘A)
**WindowMenu**:
- Minimise (⌘M)
- Zoom
- ---
- Bring All to Front
**HelpMenu**:
- [App Name] Help
**EditMenu**:
- Undo (Ctrl+Z)
- Redo (Ctrl+Y)
- ---
- Cut (Ctrl+X)
- Copy (Ctrl+C)
- Paste (Ctrl+V)
- Select All (Ctrl+A)
**WindowMenu**:
- Minimise
- Maximise
**HelpMenu**:
- About [App Name]
Menu.AddRole(role) returns the receiver menu (the top-level menu), not the role's submenu. To add items to the role's submenu, look up the inserted role item with FindByRole and call GetSubmenu() on it:
menu.AddRole(application.FileMenu)
fileMenu := menu.FindByRole(application.FileMenu).GetSubmenu()
fileMenu.Add("Import...").OnClick(handleImport)
fileMenu.Add("Export...").OnClick(handleExport)
Create your own menus for application-specific features:
// Add a custom top-level menu
toolsMenu := menu.AddSubmenu("Tools")
// Add items
toolsMenu.Add("Settings").OnClick(func(ctx *application.Context) {
showSettingsWindow()
})
toolsMenu.AddSeparator()
// Add checkbox
toolsMenu.AddCheckbox("Dark Mode", false).OnClick(func(ctx *application.Context) {
isDark := ctx.ClickedMenuItem().Checked()
setTheme(isDark)
})
// Add radio group
toolsMenu.AddRadio("Small", true).OnClick(handleFontSize)
toolsMenu.AddRadio("Medium", false).OnClick(handleFontSize)
toolsMenu.AddRadio("Large", false).OnClick(handleFontSize)
// Add submenu
advancedMenu := toolsMenu.AddSubmenu("Advanced")
advancedMenu.Add("Configure...").OnClick(showAdvancedSettings)
For more menu item types, see Menu Reference.
Update menus based on application state:
var saveMenuItem *application.MenuItem
func createMenu() {
menu := app.NewMenu()
fileMenu := menu.AddSubmenu("File")
saveMenuItem = fileMenu.Add("Save")
saveMenuItem.SetEnabled(false) // Initially disabled
saveMenuItem.OnClick(handleSave)
app.Menu.Set(menu)
}
func onDocumentChanged() {
saveMenuItem.SetEnabled(hasUnsavedChanges())
menu.Update() // Important!
}
:::caution[Always Call menu.Update()]
After changing menu state (enable/disable, label, checked), always call menu.Update(). This is especially critical on Windows where menus are reconstructed.
See Menu Reference for details. :::
updateMenuItem := menu.Add("Check for Updates")
updateMenuItem.OnClick(func(ctx *application.Context) {
updateMenuItem.SetLabel("Checking...")
menu.Update()
checkForUpdates()
updateMenuItem.SetLabel("Check for Updates")
menu.Update()
})
For major changes, rebuild the entire menu:
func rebuildFileMenu() {
menu := app.NewMenu()
fileMenu := menu.AddSubmenu("File")
fileMenu.Add("New").OnClick(handleNew)
fileMenu.Add("Open").OnClick(handleOpen)
// Add recent files dynamically
if hasRecentFiles() {
recentMenu := fileMenu.AddSubmenu("Open Recent")
for _, file := range getRecentFiles() {
filePath := file // Capture for closure
recentMenu.Add(filepath.Base(file)).OnClick(func(ctx *application.Context) {
openFile(filePath)
})
}
recentMenu.AddSeparator()
recentMenu.Add("Clear Recent").OnClick(clearRecentFiles)
}
fileMenu.AddSeparator()
fileMenu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
})
app.Menu.Set(menu)
}
Menu items can control windows:
viewMenu := menu.AddSubmenu("View")
// Toggle fullscreen
viewMenu.Add("Toggle Fullscreen").OnClick(func(ctx *application.Context) {
if window, ok := app.Window.GetByName("main"); ok {
window.ToggleFullscreen()
}
})
// Zoom controls
viewMenu.Add("Zoom In").SetAccelerator("CmdOrCtrl++").OnClick(func(ctx *application.Context) {
// Increase zoom
})
viewMenu.Add("Zoom Out").SetAccelerator("CmdOrCtrl+-").OnClick(func(ctx *application.Context) {
// Decrease zoom
})
viewMenu.Add("Reset Zoom").SetAccelerator("CmdOrCtrl+0").OnClick(func(ctx *application.Context) {
// Reset zoom
})
Get the active window:
menuItem.OnClick(func(ctx *application.Context) {
window := application.Get().Window.Current() // the window the menu was invoked from
// Use window
})
Menu bar behaviour:
menu.AddRole(application.AppMenu) for standard itemsStandard locations:
Example:
if runtime.GOOS == "darwin" {
menu.AddRole(application.AppMenu) // Adds About, Preferences, Quit
// Don't add Quit to File menu on macOS
// Don't add About to Help menu on macOS
}
Menu bar behaviour:
Standard locations:
Example:
if runtime.GOOS == "windows" {
menu.AddRole(application.FileMenu) // Exit is added automatically
menu.AddRole(application.HelpMenu) // About is added automatically
}
Menu bar behaviour:
Best practice: Follow Windows conventions, test on target DEs.
Here's a production-ready menu structure:
package main
import (
"runtime"
"github.com/wailsapp/wails/v3/pkg/application"
)
func main() {
app := application.New(application.Options{
Name: "My Application",
})
// Create and set menu
createMenu(app)
// Create main window with UseApplicationMenu for cross-platform menu support
app.Window.NewWithOptions(application.WebviewWindowOptions{
UseApplicationMenu: true,
})
app.Run()
}
func createMenu(app *application.App) {
menu := app.NewMenu()
// Platform-specific application menu (macOS only)
if runtime.GOOS == "darwin" {
menu.AddRole(application.AppMenu)
}
// File menu — AddRole returns the receiver menu, not the role submenu.
// To add items into the File submenu, look it up via FindByRole + GetSubmenu.
menu.AddRole(application.FileMenu)
fileMenu := menu.FindByRole(application.FileMenu).GetSubmenu()
fileMenu.Add("Import...").SetAccelerator("CmdOrCtrl+I").OnClick(handleImport)
fileMenu.Add("Export...").SetAccelerator("CmdOrCtrl+E").OnClick(handleExport)
// Edit menu
menu.AddRole(application.EditMenu)
// View menu
viewMenu := menu.AddSubmenu("View")
viewMenu.Add("Toggle Fullscreen").SetAccelerator("F11").OnClick(toggleFullscreen)
viewMenu.AddSeparator()
viewMenu.AddCheckbox("Show Sidebar", true).OnClick(toggleSidebar)
viewMenu.AddCheckbox("Show Toolbar", true).OnClick(toggleToolbar)
// Tools menu
toolsMenu := menu.AddSubmenu("Tools")
// Settings location varies by platform
if runtime.GOOS == "darwin" {
// On macOS, Preferences is in Application menu (added by AppMenu role)
} else {
toolsMenu.Add("Settings").SetAccelerator("CmdOrCtrl+,").OnClick(showSettings)
}
toolsMenu.AddSeparator()
toolsMenu.AddCheckbox("Dark Mode", false).OnClick(toggleDarkMode)
// Window menu
menu.AddRole(application.WindowMenu)
// Help menu
helpMenu := menu.AddRole(application.HelpMenu)
helpMenu.Add("Documentation").OnClick(openDocumentation)
// About location varies by platform
if runtime.GOOS == "darwin" {
// On macOS, About is in Application menu (added by AppMenu role)
} else {
helpMenu.AddSeparator()
helpMenu.Add("About").OnClick(showAbout)
}
// Set the application menu
app.Menu.Set(menu)
}
func handleImport(ctx *application.Context) {
// Implementation
}
func handleExport(ctx *application.Context) {
// Implementation
}
func toggleFullscreen(ctx *application.Context) {
window := application.Get().Window.Current()
window.ToggleFullscreen()
}
func toggleSidebar(ctx *application.Context) {
// Implementation
}
func toggleToolbar(ctx *application.Context) {
// Implementation
}
func showSettings(ctx *application.Context) {
// Implementation
}
func toggleDarkMode(ctx *application.Context) {
isDark := ctx.ClickedMenuItem().Checked()
// Apply theme
}
func openDocumentation(ctx *application.Context) {
// Open browser
}
func showAbout(ctx *application.Context) {
// Show about dialog
}
CmdOrCtrl[Learn More →](/features/menus/reference)
[Learn More →](/features/menus/context)
[Learn More →](/features/menus/systray)
[Learn More →](/guides/menus)
Questions? Ask in Discord or check the menu example.