Pure Go system tray library for Windows, macOS, and Linux
Zero CGO. Cross-platform. Multiple trays. Context menus. Notifications.
- Pure Go — zero CGO on all platforms. Single binary, easy cross-compilation
- Multiple trays — create as many tray icons as you need
- Context menus — nested menus with checkboxes, separators, icons, and submenus
- Notifications — balloon tips (Windows), notification center (macOS), D-Bus notifications (Linux)
- Dark mode — automatic icon switching for light/dark themes (Windows)
- Template icons — macOS-native monochrome icons that adapt to system theme
- Builder pattern — fluent API for clean, readable code
- Message loop — built-in
Run()blocks and pumps the platform event loop - Standalone — no dependency on gogpu framework. Use in any Go application
| Platform | API | Dependency | Status |
|---|---|---|---|
| Windows | Shell_NotifyIconW (shell32.dll) |
golang.org/x/sys/windows |
Implemented |
| macOS | NSStatusBar / NSStatusItem (AppKit) |
github.com/go-webgpu/goffi |
Implemented |
| Linux | StatusNotifierItem (D-Bus SNI) | github.com/godbus/dbus/v5 |
Implemented |
All platform implementations use Pure Go FFI — no C compiler required.
go get github.com/gogpu/systrayRequirements: Go 1.25+
package main
import (
"fmt"
"os"
"github.com/gogpu/systray"
)
func main() {
tray := systray.New()
// Build context menu
menu := systray.NewMenu()
menu.Add("Hello", func() { fmt.Println("Hello clicked!") })
menu.Add("Show Notification", func() {
tray.ShowNotification("My App", "Hello from systray!")
})
menu.AddSeparator()
menu.AddCheckbox("Check me", false, func() { fmt.Println("Toggled") })
menu.AddSeparator()
menu.Add("Quit", func() {
tray.Remove()
os.Exit(0)
})
// Configure and show
tray.SetIcon(iconPNG).
SetTooltip("My Application").
SetMenu(menu)
tray.OnClick(func() { fmt.Println("Left click!") })
tray.Show()
// Run the platform message loop (blocks until Quit)
if err := tray.Run(); err != nil {
fmt.Println("error:", err)
}
}// Create and lifecycle
tray := systray.New() // Create a new system tray icon
tray.Show() // Show tray icon
tray.Hide() // Hide tray icon (without removing)
tray.Run() // Block and pump the platform message loop
tray.Remove() // Destroy tray icon and release resources
// Icon management
tray.SetIcon(png []byte) // Set tray icon (PNG format)
tray.SetDarkModeIcon(png []byte) // Auto-switch in dark mode (Windows)
tray.SetTemplateIcon(png []byte) // macOS template image (monochrome)
// Text and menu
tray.SetTooltip(text string) // Hover tooltip
tray.SetMenu(menu *Menu) // Attach context menu
// Events
tray.OnClick(fn func()) // Left click handler
tray.OnDoubleClick(fn func()) // Double click handler
tray.OnRightClick(fn func()) // Right click handler
// Notifications
tray.ShowNotification(title, message string) // OS-level notification
// Position (Windows only; macOS/Linux return zeros)
x, y, w, h := tray.Bounds() // Tray icon screen positionAll setter methods return *SystemTray for fluent chaining:
tray.SetIcon(icon).SetTooltip("Ready").SetMenu(menu).Show()menu := systray.NewMenu()
item := menu.Add("Label", onClick) // Normal item → *MenuItem
check := menu.AddCheckbox("Toggle", checked, onChange) // Checkbox → *MenuItem
menu.AddSeparator() // Separator → *Menu (chaining)
sub := menu.AddSubmenu("More", submenu) // Submenu → *MenuItem
icon := menu.AddWithIcon("Save", iconPNG, onClick) // With icon → *MenuItemAdd, AddCheckbox, AddSubmenu, AddWithIcon return *MenuItem for dynamic updates. AddSeparator returns *Menu for chaining.
Update menu items at runtime from any goroutine — thread-safe, changes are applied in-place via native platform APIs (no menu rebuild). On macOS, updates are automatically dispatched to the main thread:
item.SetLabel("New Label") // Change display text
check.SetChecked(false) // Change checked state
sub.SetDisabled(true) // Disable/enable
icon.SetIcon(newIconPNG) // Change icon
// Thread-safe getters
if check.IsChecked() { ... } // Read current state
if sub.IsDisabled() { ... }// Each tray is independent with its own icon, menu, and handlers
mainTray := systray.New().SetIcon(appIcon).SetMenu(mainMenu).Show()
statusTray := systray.New().SetIcon(statusIcon).SetTooltip("Status: OK").Show()systray supports automatic icon switching based on the system theme.
Windows — Use SetDarkModeIcon() to provide an alternative icon for dark mode. The library detects theme changes via WM_SETTINGCHANGE with "ImmersiveColorSet" and switches icons automatically:
tray.SetIcon(lightIcon).SetDarkModeIcon(darkIcon)macOS — Use SetTemplateIcon() with a monochrome PNG. macOS renders template images with the correct color for the current menu bar appearance (light or dark). Only the alpha channel matters:
tray.SetTemplateIcon(monochromeIcon)Linux — The SNI protocol delivers the icon pixmap to the desktop environment, which handles theme adaptation. No special API is needed.
ShowNotification sends an OS-level notification from the tray icon:
tray.ShowNotification("Update Available", "Version 2.0 is ready to install.")| Platform | Mechanism | Notes |
|---|---|---|
| Windows | Balloon tip (Shell_NotifyIconW + NIF_INFO) |
Appears near the tray icon |
| macOS | NSUserNotification / Notification Center |
Requires notification permission on macOS 13+ |
| Linux | org.freedesktop.Notifications D-Bus |
Works on GNOME, KDE, XFCE, and other FreeDesktop-compliant DEs |
| Platform | Recommended Size | Format | Notes |
|---|---|---|---|
| Windows | 16x16, 32x32 | PNG | Provide both sizes for standard and HiDPI |
| macOS | 22x22, 44x44 (@2x) | PNG | Must be monochrome (template) for proper theme adaptation |
| Linux | 22x22, 24x24 | PNG | SNI spec recommends 22x22 |
Input format: PNG bytes ([]byte). The library handles conversion to native format (HICON, NSImage, ARGB pixmap) internally.
For macOS, use SetTemplateIcon() with a monochrome PNG (only alpha channel matters). The system automatically adjusts the icon color for light/dark menu bar.
systray.New() -> SystemTray (public API)
|
PlatformTray (internal interface)
|
+------------+------------+
| | |
Win32 impl macOS impl Linux impl
Shell_Notify NSStatusBar D-Bus SNI
IconW NSStatusItem StatusNotifierItem
Follows the Qt6 QPlatformSystemTrayIcon three-layer pattern. Each platform implementation is isolated in its own file with build constraints.
While systray is fully standalone, it integrates with the gogpu application framework. systray runs its own message loop via Run(), or you can manage the lifecycle manually alongside gogpu's event loop:
import (
"github.com/gogpu/systray"
)
// Create tray icon alongside your gogpu app
tray := systray.New()
tray.SetIcon(icon).SetMenu(menu).Show()
// Show/hide window on tray click
tray.OnClick(func() {
fmt.Println("Tray clicked — toggle window")
})| Feature | gogpu/systray | getlantern/systray | fyne-io/systray |
|---|---|---|---|
| Pure Go (zero CGO) | Yes | No (CGO on macOS/Linux) | No (CGO on macOS/Linux) |
| Multiple trays | Yes | No (single global) | No (single global) |
| Dynamic menu updates | Yes (SetLabel/SetChecked/SetDisabled) | Yes (SetTitle/Check/Uncheck) | Yes (SetTitle/Check/Uncheck) |
| Dark mode auto-switch | Yes (Windows) | No | No |
| Template icons (macOS) | Yes | Yes | Yes |
| Nested menus | Yes | Yes | Yes |
| Menu item icons | Yes | Yes | Yes |
| Notifications | Yes | No | No |
| Builder pattern | Yes | No | No |
| Built-in message loop | Yes | Yes | Yes |
| Wayland support | Yes (D-Bus SNI) | Yes (D-Bus SNI) | Yes (D-Bus SNI) |
We welcome contributions! See CONTRIBUTING.md for guidelines.
systray is part of the GoGPU ecosystem — 1.2M+ lines of Pure Go, zero CGO. A GPU computing platform with a WebGPU implementation, shader compiler, 2D graphics library, and GUI toolkit.
| Library | Purpose |
|---|---|
| gogpu | Application framework, windowing |
| wgpu | Pure Go WebGPU (Vulkan/Metal/DX12/GLES) |
| naga | Shader compiler (WGSL to SPIR-V/MSL/GLSL/HLSL/DXIL) |
| gg | 2D graphics with GPU acceleration |
| ui | GUI toolkit (27 widgets, 4 themes) |
| systray | System tray (this library) |
MIT License — see LICENSE for details.