Application Menus

The Problem

Professional desktop applications need menu bars—File, Edit, View, Help. But menus work differently on each platform:

  • macOS: Global menu bar at top of screen
  • Windows: Menu bar in window title bar
  • Linux: Varies by desktop environment

Building platform-appropriate menus manually is tedious and error-prone.

The Wails Solution

Wails provides a unified API that creates platform-native menus automatically. Write once, get native behaviour on all platforms.

A Wails application menu on macOS with standard, checkbox, radio, and submenu items

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.

Quick Start

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.

Creating Menus

Basic Menu Creation

// 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()
})

Setting the Menu

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:

  • On macOS: The menu appears at the top of screen (standard behaviour)
  • On Windows/Linux: Each window with UseApplicationMenu: true displays the app menu

Platform-specific details:

Global menu bar (one per application):

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.

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.

Available Roles

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

Using Roles

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:

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

Customising Role Menus

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)

Custom Menus

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.

Dynamic Menus

Update menus based on application state:

Enable/Disable Items

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!
}

Change Labels

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()
})

Rebuild Menus

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)
}

Window Control from Menus

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
})

Platform-Specific Considerations

macOS

Menu bar behaviour:

  • Appears at top of screen (global)
  • Persists when all windows closed
  • First menu is always the application menu
  • Use menu.AddRole(application.AppMenu) for standard items

Standard locations:

  • About: Application menu
  • Preferences: Application menu (⌘,)
  • Quit: Application menu (⌘Q)
  • Help: Help menu

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
}

Windows

Menu bar behaviour:

  • Appears in window title bar
  • Each window has its own menu
  • No application menu

Standard locations:

  • Exit: File menu (Alt+F4)
  • Settings: Tools or Edit menu
  • About: Help menu

Example:

if runtime.GOOS == "windows" {
    menu.AddRole(application.FileMenu) // Exit is added automatically
    menu.AddRole(application.HelpMenu) // About is added automatically
}

Linux

Menu bar behaviour:

  • Usually per-window (like Windows)
  • Some DEs support global menus (Unity, GNOME with extension)
  • Appearance varies by desktop environment

Best practice: Follow Windows conventions, test on target DEs.

Complete Example

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
}

Best Practices

✅ Do

  • Use menu roles for standard menus (File, Edit, etc.)
  • Follow platform conventions for menu structure
  • Add keyboard shortcuts to common actions
  • Call menu.Update() after changing menu state
  • Test on all platforms - behaviour varies
  • Keep menus shallow - 2-3 levels maximum
  • Use clear labels - “Save Project” not “Save”

❌ Don’t

  • Don’t hardcode platform shortcuts - Use CmdOrCtrl
  • Don’t put Quit in File menu on macOS - It’s in Application menu
  • Don’t put About in Help menu on macOS - It’s in Application menu
  • Don’t forget menu.Update() - Menus won’t work properly
  • Don’t nest too deeply - Users get lost
  • Don’t use jargon - Keep labels user-friendly

Next Steps

Menu Reference

Complete reference for menu item types and properties.

Learn More →

Context Menus

Create right-click context menus.

Learn More →

System Tray Menus

Add system tray/menu bar integration.

Learn More →

Menu Patterns

Common menu patterns and best practices.

Learn More →


Questions? Ask in Discord or check the menu example.

Edit page

Last updated: