713 lines
16 KiB
Text
713 lines
16 KiB
Text
---
|
|
title: System Tray Menus
|
|
description: Add system tray (notification area) integration to your application
|
|
sidebar:
|
|
order: 3
|
|
---
|
|
|
|
import { Tabs, TabItem, Card, CardGrid } from "@astrojs/starlight/components";
|
|
|
|
## 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.
|
|
{/* VISUAL PLACEHOLDER: System Tray Comparison
|
|
Description: Three screenshots showing the same Wails system tray icon on:
|
|
1. Windows - Notification area (bottom-right)
|
|
2. macOS - Menu bar (top-right) with label
|
|
3. Linux (GNOME) - Top bar
|
|
All showing the same icon and menu structure
|
|
Style: Clean screenshots with arrows pointing to tray icon, menu expanded
|
|
*/}
|
|
|
|
## Quick Start
|
|
|
|
```go
|
|
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.
|
|
|
|
## Creating a System Tray
|
|
|
|
### Basic System Tray
|
|
|
|
```go
|
|
// Create system tray
|
|
systray := app.SystemTray.New()
|
|
|
|
// Set icon
|
|
systray.SetIcon(iconBytes)
|
|
|
|
// Set label (macOS) / tooltip (Windows)
|
|
systray.SetLabel("My Application")
|
|
```
|
|
|
|
### With Icon
|
|
|
|
Icons should be embedded:
|
|
|
|
```go
|
|
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:**
|
|
|
|
| Platform | Size | Format | Notes |
|
|
|----------|------|--------|-------|
|
|
| **Windows** | 16x16 or 32x32 | PNG, ICO | Notification area |
|
|
| **macOS** | 18x18 to 22x22 | PNG | Menu bar, template recommended |
|
|
| **Linux** | 22x22 to 48x48 | PNG, SVG | Varies by DE |
|
|
|
|
### Template Icons (macOS)
|
|
|
|
Template icons adapt to light/dark mode automatically:
|
|
|
|
```go
|
|
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](https://bjango.com/articles/designingmenubarextras/)
|
|
|
|
## Adding Menus
|
|
|
|
System tray menus work like application menus:
|
|
|
|
```go
|
|
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](/features/menus/reference).
|
|
|
|
## Attaching Windows
|
|
|
|
Attach a window to the tray icon for automatic show/hide:
|
|
|
|
```go
|
|
// 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**
|
|
|
|
```go
|
|
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)
|
|
```
|
|
|
|
## Click Handlers
|
|
|
|
Handle tray icon clicks:
|
|
|
|
```go
|
|
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:**
|
|
|
|
| Event | Windows | macOS | Linux |
|
|
|-------|---------|-------|-------|
|
|
| OnClick | ✅ | ✅ | ✅ |
|
|
| OnRightClick | ✅ | ✅ | ✅ |
|
|
| OnDoubleClick | ✅ | ✅ | ⚠️ Varies |
|
|
| OnMouseEnter | ✅ | ✅ | ⚠️ Varies |
|
|
| OnMouseLeave | ✅ | ✅ | ⚠️ Varies |
|
|
|
|
## Dynamic Updates
|
|
|
|
Update tray icon and menu dynamically:
|
|
|
|
### Change Icon
|
|
|
|
```go
|
|
var isActive bool
|
|
|
|
func updateTrayIcon() {
|
|
if isActive {
|
|
systray.SetIcon(activeIcon)
|
|
systray.SetLabel("Active")
|
|
} else {
|
|
systray.SetIcon(inactiveIcon)
|
|
systray.SetLabel("Inactive")
|
|
}
|
|
}
|
|
```
|
|
|
|
### Update Menu
|
|
|
|
```go
|
|
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!
|
|
})
|
|
```
|
|
|
|
:::caution[Always Call Update()]
|
|
After changing menu state, **call `menu.Update()`**. See [Menu Reference](/features/menus/reference#enabled-state).
|
|
:::
|
|
|
|
### Rebuild Menu
|
|
|
|
For major changes, rebuild the entire menu:
|
|
|
|
```go
|
|
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)
|
|
}
|
|
```
|
|
|
|
## Platform-Specific Features
|
|
|
|
<Tabs syncKey="platform">
|
|
<TabItem label="macOS" icon="apple">
|
|
**Menu bar integration:**
|
|
|
|
```go
|
|
// 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
|
|
</TabItem>
|
|
|
|
<TabItem label="Windows" icon="seti:windows">
|
|
**Notification area integration:**
|
|
|
|
```go
|
|
// Set tooltip (appears on hover)
|
|
systray.SetTooltip("My Application")
|
|
|
|
// Or use SetLabel (same as tooltip on Windows)
|
|
systray.SetLabel("My Application")
|
|
|
|
// Show/Hide functionality (fully functional)
|
|
systray.Show() // Show tray icon
|
|
systray.Hide() // Hide tray icon
|
|
```
|
|
|
|
**Icon requirements:**
|
|
- 16x16 or 32x32 pixels
|
|
- PNG or ICO format
|
|
- Transparent background
|
|
|
|
**Tooltip limits:**
|
|
- Maximum 127 UTF-16 characters
|
|
- Longer tooltips will be truncated
|
|
- Keep concise for best experience
|
|
|
|
**Platform features:**
|
|
- Tray icon survives Windows Explorer restarts
|
|
- Show() and Hide() methods fully functional
|
|
- Proper lifecycle management
|
|
|
|
**Best practices:**
|
|
- Use 32x32 for high-DPI displays
|
|
- Keep tooltips under 127 characters
|
|
- Test on different Windows versions
|
|
- Consider notification area overflow
|
|
- Use Show/Hide for conditional tray visibility
|
|
</TabItem>
|
|
|
|
<TabItem label="Linux" icon="linux">
|
|
**System tray integration:**
|
|
|
|
Uses StatusNotifierItem specification (most modern DEs).
|
|
|
|
```go
|
|
systray.SetIcon(iconBytes)
|
|
systray.SetLabel("My App")
|
|
```
|
|
|
|
**Desktop environment support:**
|
|
- **GNOME**: Top bar (with extension)
|
|
- **KDE Plasma**: System tray
|
|
- **XFCE**: Notification area
|
|
- **Others**: Varies
|
|
|
|
**Best practices:**
|
|
- Use 22x22 or 24x24 pixels
|
|
- SVG icons scale better
|
|
- Test on target desktop environments
|
|
- Provide fallback for unsupported DEs
|
|
</TabItem>
|
|
</Tabs>
|
|
|
|
## Complete Example
|
|
|
|
Here's a production-ready system tray application:
|
|
|
|
```go
|
|
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)
|
|
}
|
|
```
|
|
|
|
## Visibility Control
|
|
|
|
Show/hide the tray icon dynamically:
|
|
|
|
```go
|
|
// Hide tray icon
|
|
systray.Hide()
|
|
|
|
// Show tray icon
|
|
systray.Show()
|
|
|
|
// Check visibility
|
|
if systray.IsVisible() {
|
|
fmt.Println("Tray icon is visible")
|
|
}
|
|
```
|
|
|
|
**Platform Support:**
|
|
|
|
| Platform | Hide() | Show() | Notes |
|
|
|----------|--------|--------|-------|
|
|
| **Windows** | ✅ | ✅ | Fully functional - icon appears/disappears from notification area |
|
|
| **macOS** | ✅ | ✅ | Menu bar item shows/hides |
|
|
| **Linux** | ✅ | ✅ | Varies 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:**
|
|
|
|
```go
|
|
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()
|
|
}
|
|
}
|
|
```
|
|
|
|
## Cleanup
|
|
|
|
Destroy the tray icon when done:
|
|
|
|
```go
|
|
// In OnShutdown
|
|
app := application.New(application.Options{
|
|
OnShutdown: func() {
|
|
if systray != nil {
|
|
systray.Destroy()
|
|
}
|
|
},
|
|
})
|
|
```
|
|
|
|
**Important:** Always destroy system tray on shutdown to release resources.
|
|
|
|
## Best Practices
|
|
|
|
### ✅ Do
|
|
|
|
- **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
|
|
|
|
- **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
|
|
|
|
## Troubleshooting
|
|
|
|
### Tray Icon Not Appearing
|
|
|
|
**Possible causes:**
|
|
1. Icon format not supported
|
|
2. Icon size too large/small
|
|
3. System tray not supported (Linux)
|
|
|
|
**Solution:**
|
|
|
|
```go
|
|
// Check if system tray is supported
|
|
if !application.SystemTraySupported() {
|
|
fmt.Println("System tray not supported")
|
|
// Fallback to window-only mode
|
|
}
|
|
```
|
|
|
|
### Icon Looks Wrong on macOS
|
|
|
|
**Cause:** Not using template icon
|
|
|
|
**Solution:**
|
|
|
|
```go
|
|
// Use template icon
|
|
systray.SetTemplateIcon(iconBytes)
|
|
|
|
// Or design icon as template (black + transparent)
|
|
```
|
|
|
|
### Menu Not Updating
|
|
|
|
**Cause:** Forgot to call `menu.Update()`
|
|
|
|
**Solution:**
|
|
|
|
```go
|
|
menuItem.SetLabel("New Label")
|
|
menu.Update() // Add this!
|
|
```
|
|
|
|
## Next Steps
|
|
|
|
<CardGrid>
|
|
<Card title="Menu Reference" icon="document">
|
|
Complete reference for menu item types and properties.
|
|
|
|
[Learn More →](/features/menus/reference)
|
|
</Card>
|
|
|
|
<Card title="Application Menus" icon="list-format">
|
|
Create application menu bars.
|
|
|
|
[Learn More →](/features/menus/application)
|
|
</Card>
|
|
|
|
<Card title="Context Menus" icon="puzzle">
|
|
Create right-click context menus.
|
|
|
|
[Learn More →](/features/menus/context)
|
|
</Card>
|
|
|
|
<Card title="System Tray Tutorial" icon="open-book">
|
|
Build a complete system tray application.
|
|
|
|
[Learn More →](/tutorials/system-tray)
|
|
</Card>
|
|
</CardGrid>
|
|
|
|
---
|
|
|
|
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [system tray examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/systray-basic).
|