Skip to content

System Tray Menus

Wails provides unified system tray APIs that work across all platforms. Create tray icons with menus, attach windows, and handle clicks with native platform behaviour for background applications, services, and quick-access utilities.

package main
import (
_ "embed"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed assets/icon.png
var icon []byte
func main() {
app := application.New(application.Options{
Name: "Tray App",
})
// Create system tray
systray := app.SystemTray.New()
systray.SetIcon(icon)
systray.SetLabel("My App")
// Add menu
menu := app.NewMenu()
menu.Add("Show").OnClick(func(ctx *application.Context) {
// Show main window
})
menu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
})
systray.SetMenu(menu)
// Create hidden window
window := app.Window.New()
window.Hide()
app.Run()
}

Result: System tray icon with menu on all platforms.

// Create system tray
systray := app.SystemTray.New()
// Set icon
systray.SetIcon(iconBytes)
// Set label (macOS) / tooltip (Windows)
systray.SetLabel("My Application")

Icons should be embedded:

import _ "embed"
//go:embed assets/icon.png
var icon []byte
//go:embed assets/icon-dark.png
var iconDark []byte
func main() {
app := application.New(application.Options{
Name: "My App",
})
systray := app.SystemTray.New()
systray.SetIcon(icon)
systray.SetDarkModeIcon(iconDark) // macOS dark mode
app.Run()
}

Icon requirements:

PlatformSizeFormatNotes
Windows16x16 or 32x32PNG, ICONotification area
macOS18x18 to 22x22PNGMenu bar, template recommended
Linux22x22 to 48x48PNG, SVGVaries by DE

Template icons adapt to light/dark mode automatically:

systray.SetTemplateIcon(iconBytes)

Template icon guidelines:

  • Use black and clear (transparent) colours only
  • Black becomes white in dark mode
  • Name file with Template suffix: iconTemplate.png
  • Design guide

System tray menus work like application menus:

menu := app.NewMenu()
// Add items
menu.Add("Open").OnClick(func(ctx *application.Context) {
showMainWindow()
})
menu.AddSeparator()
menu.AddCheckbox("Start at Login", false).OnClick(func(ctx *application.Context) {
enabled := ctx.ClickedMenuItem().Checked()
setStartAtLogin(enabled)
})
menu.AddSeparator()
menu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
})
// Set menu
systray.SetMenu(menu)

For all menu item types, see Menu Reference.

Attach a window to the tray icon for automatic show/hide:

// Create window
window := app.Window.New()
// Attach to tray
systray.AttachWindow(window)
// Configure behaviour
systray.SetWindowOffset(10) // Pixels from tray icon
systray.SetWindowDebounce(200 * time.Millisecond) // Click debounce

Behaviour:

  • Window starts hidden
  • Left-click tray icon → Toggle window visibility
  • Right-click tray icon → Show menu (if set)
  • Window positioned near tray icon

Example: Popup window

window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Quick Access",
Width: 300,
Height: 400,
Frameless: true, // No title bar
AlwaysOnTop: true, // Stay on top
})
systray.AttachWindow(window)
systray.SetWindowOffset(5)

Handle tray icon clicks:

systray := app.SystemTray.New()
// Left click
systray.OnClick(func() {
fmt.Println("Tray icon clicked")
})
// Right click
systray.OnRightClick(func() {
fmt.Println("Tray icon right-clicked")
})
// Double click
systray.OnDoubleClick(func() {
fmt.Println("Tray icon double-clicked")
})
// Mouse enter/leave
systray.OnMouseEnter(func() {
fmt.Println("Mouse entered tray icon")
})
systray.OnMouseLeave(func() {
fmt.Println("Mouse left tray icon")
})

Platform support:

EventWindowsmacOSLinux
OnClick
OnRightClick
OnDoubleClick⚠️ Varies
OnMouseEnter⚠️ Varies
OnMouseLeave⚠️ Varies

Update tray icon and menu dynamically:

var isActive bool
func updateTrayIcon() {
if isActive {
systray.SetIcon(activeIcon)
systray.SetLabel("Active")
} else {
systray.SetIcon(inactiveIcon)
systray.SetLabel("Inactive")
}
}
var isPaused bool
pauseMenuItem := menu.Add("Pause")
pauseMenuItem.OnClick(func(ctx *application.Context) {
isPaused = !isPaused
if isPaused {
pauseMenuItem.SetLabel("Resume")
} else {
pauseMenuItem.SetLabel("Pause")
}
menu.Update() // Important!
})

For major changes, rebuild the entire menu:

func rebuildTrayMenu(status string) {
menu := app.NewMenu()
// Status-specific items
switch status {
case "syncing":
menu.Add("Syncing...").SetEnabled(false)
menu.Add("Pause Sync").OnClick(pauseSync)
case "synced":
menu.Add("Up to date ✓").SetEnabled(false)
menu.Add("Sync Now").OnClick(startSync)
case "error":
menu.Add("Sync Error").SetEnabled(false)
menu.Add("Retry").OnClick(retrySync)
}
menu.AddSeparator()
menu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
})
systray.SetMenu(menu)
}

Menu bar integration:

// Set label (appears next to icon)
systray.SetLabel("My App")
// Use template icon (adapts to dark mode)
systray.SetTemplateIcon(iconBytes)
// Set icon position
systray.SetIconPosition(application.IconPositionRight)

Icon positions:

  • IconPositionLeft - Icon left of label
  • IconPositionRight - Icon right of label
  • IconPositionOnly - Icon only, no label
  • IconPositionNone - Label only, no icon

Best practices:

  • Use template icons (black + transparent)
  • Keep labels short (3-5 characters)
  • 18x18 to 22x22 pixels for Retina displays
  • Test in both light and dark modes

Here’s a production-ready system tray application:

package main
import (
_ "embed"
"fmt"
"time"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed assets/icon.png
var icon []byte
//go:embed assets/icon-active.png
var iconActive []byte
type TrayApp struct {
app *application.Application
systray *application.SystemTray
window *application.WebviewWindow
menu *application.Menu
isActive bool
}
func main() {
app := application.New(application.Options{
Name: "Tray Application",
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: false,
},
})
trayApp := &TrayApp{app: app}
trayApp.setup()
app.Run()
}
func (t *TrayApp) setup() {
// Create system tray
t.systray = t.app.SystemTray.New()
t.systray.SetIcon(icon)
t.systray.SetLabel("Inactive")
// Create menu
t.createMenu()
// Create window (hidden by default)
t.window = t.app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Tray Application",
Width: 400,
Height: 600,
Hidden: true,
})
// Attach window to tray
t.systray.AttachWindow(t.window)
t.systray.SetWindowOffset(10)
// Handle tray clicks
t.systray.OnRightClick(func() {
t.systray.OpenMenu()
})
// Start background task
go t.backgroundTask()
}
func (t *TrayApp) createMenu() {
t.menu = t.app.NewMenu()
// Status item (disabled)
statusItem := t.menu.Add("Status: Inactive")
statusItem.SetEnabled(false)
t.menu.AddSeparator()
// Toggle active
t.menu.Add("Start").OnClick(func(ctx *application.Context) {
t.toggleActive()
})
// Show window
t.menu.Add("Show Window").OnClick(func(ctx *application.Context) {
t.window.Show()
t.window.SetFocus()
})
t.menu.AddSeparator()
// Settings
t.menu.AddCheckbox("Start at Login", false).OnClick(func(ctx *application.Context) {
enabled := ctx.ClickedMenuItem().Checked()
t.setStartAtLogin(enabled)
})
t.menu.AddSeparator()
// Quit
t.menu.Add("Quit").OnClick(func(ctx *application.Context) {
t.app.Quit()
})
t.systray.SetMenu(t.menu)
}
func (t *TrayApp) toggleActive() {
t.isActive = !t.isActive
t.updateTray()
}
func (t *TrayApp) updateTray() {
if t.isActive {
t.systray.SetIcon(iconActive)
t.systray.SetLabel("Active")
} else {
t.systray.SetIcon(icon)
t.systray.SetLabel("Inactive")
}
// Rebuild menu with new status
t.createMenu()
}
func (t *TrayApp) backgroundTask() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
if t.isActive {
fmt.Println("Background task running...")
// Do work
}
}
}
func (t *TrayApp) setStartAtLogin(enabled bool) {
// Implementation varies by platform
fmt.Printf("Start at login: %v\n", enabled)
}

Show/hide the tray icon dynamically:

// Hide tray icon
systray.Hide()
// Show tray icon
systray.Show()
// Check visibility
if systray.IsVisible() {
fmt.Println("Tray icon is visible")
}

Platform Support:

PlatformHide()Show()Notes
WindowsFully functional - icon appears/disappears from notification area
macOSMenu bar item shows/hides
LinuxVaries by desktop environment

Use cases:

  • Temporarily hide tray icon based on user preference
  • Headless mode with tray icon appearing only when needed
  • Toggle visibility based on application state

Example - Conditional Tray Visibility:

func (t *TrayApp) setTrayVisibility(visible bool) {
if visible {
t.systray.Show()
} else {
t.systray.Hide()
}
}
// Show tray only when updates are available
func (t *TrayApp) checkForUpdates() {
if hasUpdates {
t.systray.Show()
t.systray.SetLabel("Update Available")
} else {
t.systray.Hide()
}
}

Destroy the tray icon when done:

// In OnShutdown
app := application.New(application.Options{
OnShutdown: func() {
if systray != nil {
systray.Destroy()
}
},
})

Important: Always destroy system tray on shutdown to release resources.

  • Use template icons on macOS - Adapts to dark mode
  • Keep labels short - 3-5 characters maximum
  • Provide tooltips on Windows - Helps users identify your app
  • Test on all platforms - Behaviour varies
  • Handle clicks appropriately - Left-click for main action, right-click for menu
  • Update icon for status - Visual feedback is important
  • Destroy on shutdown - Release resources
  • Don’t use large icons - Follow platform guidelines
  • Don’t use long labels - Gets truncated
  • Don’t forget dark mode - Test on macOS dark mode
  • Don’t block click handlers - Keep them fast
  • Don’t forget menu.Update() - After changing menu state
  • Don’t assume tray support - Some Linux DEs don’t support it

Possible causes:

  1. Icon format not supported
  2. Icon size too large/small
  3. System tray not supported (Linux)

Solution:

// Check if system tray is supported
if !application.SystemTraySupported() {
fmt.Println("System tray not supported")
// Fallback to window-only mode
}

Cause: Not using template icon

Solution:

// Use template icon
systray.SetTemplateIcon(iconBytes)
// Or design icon as template (black + transparent)

Cause: Forgot to call menu.Update()

Solution:

menuItem.SetLabel("New Label")
menu.Update() // Add this!

Menu Reference

Complete reference for menu item types and properties.

Learn More →

System Tray Tutorial

Build a complete system tray application.

Learn More →


Questions? Ask in Discord or check the system tray examples.