--- title: Context Menus description: Create right-click context menus for your application sidebar: order: 2 --- import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components"; ## The Problem Users expect right-click menus with context-specific actions. Different elements need different menus: - **Text**: Cut, Copy, Paste - **Images**: Save, Copy, Open - **Custom elements**: Application-specific actions Building context menus manually means handling mouse events, positioning, and platform differences. ## The Wails Solution Wails provides **declarative context menus** using CSS properties. Associate menus with HTML elements, pass data, and handle clicksโ€”all with native platform behaviour. ## Quick Start **Go code:** ```go // Create context menu contextMenu := app.NewContextMenu() contextMenu.Add("Cut").OnClick(handleCut) contextMenu.Add("Copy").OnClick(handleCopy) contextMenu.Add("Paste").OnClick(handlePaste) // Register with ID app.RegisterContextMenu("editor-menu", contextMenu) ``` **HTML:** ```html ``` **That's it!** Right-clicking the textarea shows your custom menu. ## Creating Context Menus ### Basic Context Menu ```go // Create menu contextMenu := app.NewContextMenu() // Add items contextMenu.Add("Cut").SetAccelerator("CmdOrCtrl+X").OnClick(func(ctx *application.Context) { // Handle cut }) contextMenu.Add("Copy").SetAccelerator("CmdOrCtrl+C").OnClick(func(ctx *application.Context) { // Handle copy }) contextMenu.Add("Paste").SetAccelerator("CmdOrCtrl+V").OnClick(func(ctx *application.Context) { // Handle paste }) // Register with unique ID app.RegisterContextMenu("text-menu", contextMenu) ``` **Menu ID:** Must be unique. Used to associate menu with HTML elements. ### With Submenus ```go contextMenu := app.NewContextMenu() // Add regular items contextMenu.Add("Open").OnClick(handleOpen) contextMenu.Add("Delete").OnClick(handleDelete) contextMenu.AddSeparator() // Add submenu exportMenu := contextMenu.AddSubmenu("Export As") exportMenu.Add("PNG").OnClick(exportPNG) exportMenu.Add("JPEG").OnClick(exportJPEG) exportMenu.Add("SVG").OnClick(exportSVG) app.RegisterContextMenu("image-menu", contextMenu) ``` ### With Checkboxes and Radio Groups ```go contextMenu := app.NewContextMenu() // Checkbox contextMenu.AddCheckbox("Show Grid", true).OnClick(func(ctx *application.Context) { showGrid := ctx.ClickedMenuItem().Checked() // Toggle grid }) contextMenu.AddSeparator() // Radio group contextMenu.AddRadio("Small", false).OnClick(handleSize) contextMenu.AddRadio("Medium", true).OnClick(handleSize) contextMenu.AddRadio("Large", false).OnClick(handleSize) app.RegisterContextMenu("view-menu", contextMenu) ``` **For all menu item types**, see [Menu Reference](/features/menus/reference). ## Associating with HTML Elements Use CSS custom properties to attach context menus: ### Basic Association ```html
Right-click me!
``` **CSS property:** `--custom-contextmenu: ` ### With Context Data Pass data from HTML to Go: ```html
Right-click this file
``` **Go handler:** ```go contextMenu := app.NewContextMenu() contextMenu.Add("Open").OnClick(func(ctx *application.Context) { fileID := ctx.ContextMenuData() // "file-123" openFile(fileID) }) app.RegisterContextMenu("file-menu", contextMenu) ``` **CSS properties:** - `--custom-contextmenu: ` - Which menu to show - `--custom-contextmenu-data: ` - Data to pass to handlers ### Dynamic Data Generate data dynamically in JavaScript: ```html
File.txt
``` ### Multiple Elements, Same Menu ```html
Document.pdf
Image.png
Video.mp4
``` **One menu, different data for each element.** ## Context Data ### Accessing Context Data ```go contextMenu.Add("Process").OnClick(func(ctx *application.Context) { data := ctx.ContextMenuData() // Get data from HTML // Use the data processItem(data) }) ``` **Data type:** Always `string`. Parse as needed. ### Passing Complex Data Use JSON for complex data: ```html
Image.png
``` **Go handler:** ```go import "encoding/json" type ItemData struct { ID int `json:"id"` Type string `json:"type"` } contextMenu.Add("Process").OnClick(func(ctx *application.Context) { dataStr := ctx.ContextMenuData() var data ItemData if err := json.Unmarshal([]byte(dataStr), &data); err != nil { log.Printf("Invalid data: %v", err) return } processItem(data.ID, data.Type) }) ``` :::caution[Security] **Always validate context data** from the frontend. Users can manipulate CSS properties, so treat data as untrusted input. ::: ### Validation Example ```go contextMenu.Add("Delete").OnClick(func(ctx *application.Context) { fileID := ctx.ContextMenuData() // Validate if !isValidFileID(fileID) { log.Printf("Invalid file ID: %s", fileID) return } // Check permissions if !canDeleteFile(fileID) { showError("Permission denied") return } // Safe to proceed deleteFile(fileID) }) ``` ## Default Context Menu The WebView provides a built-in context menu for standard operations (copy, paste, inspect). Control it with `--default-contextmenu`: ### Hide Default Menu ```html
No default menu here
``` **Use case:** Custom UI elements where default menu doesn't make sense. ### Show Default Menu ```html
Default menu always shown
``` **Use case:** Text areas, input fields, editable content. ### Auto (Smart) Mode ```html
Smart context menu
``` **Default behaviour.** Shows default menu when: - Text is selected - In text input fields - In editable content (`contenteditable`) Hides default menu otherwise. ### Combining Custom and Default ```html ``` **Behaviour:** 1. Custom menu shows first 2. If custom menu is empty or not found, default menu shows 3. Both can coexist (platform-dependent) ## Dynamic Context Menus Update menus based on application state: ### Enable/Disable Items ```go var cutMenuItem *application.MenuItem var copyMenuItem *application.MenuItem func createContextMenu() { contextMenu := app.NewContextMenu() cutMenuItem = contextMenu.Add("Cut") cutMenuItem.SetEnabled(false) // Initially disabled cutMenuItem.OnClick(handleCut) copyMenuItem = contextMenu.Add("Copy") copyMenuItem.SetEnabled(false) copyMenuItem.OnClick(handleCopy) app.RegisterContextMenu("editor-menu", contextMenu) } func onSelectionChanged(hasSelection bool) { cutMenuItem.SetEnabled(hasSelection) copyMenuItem.SetEnabled(hasSelection) contextMenu.Update() // Important! } ``` :::caution[Always Call Update()] After changing menu state, **call `contextMenu.Update()`**. This is critical on Windows. See [Menu Reference](/features/menus/reference#enabled-state) for details. ::: ### Change Labels ```go playMenuItem := contextMenu.Add("Play") playMenuItem.OnClick(func(ctx *application.Context) { if isPlaying { playMenuItem.SetLabel("Pause") } else { playMenuItem.SetLabel("Play") } contextMenu.Update() }) ``` ### Rebuild Menus For major changes, rebuild the entire menu: ```go func rebuildContextMenu(fileType string) { contextMenu := app.NewContextMenu() // Common items contextMenu.Add("Open").OnClick(handleOpen) contextMenu.Add("Delete").OnClick(handleDelete) contextMenu.AddSeparator() // Type-specific items switch fileType { case "image": contextMenu.Add("Edit Image").OnClick(editImage) contextMenu.Add("Set as Wallpaper").OnClick(setWallpaper) case "video": contextMenu.Add("Play").OnClick(playVideo) contextMenu.Add("Extract Audio").OnClick(extractAudio) case "document": contextMenu.Add("Print").OnClick(printDocument) contextMenu.Add("Export PDF").OnClick(exportPDF) } app.RegisterContextMenu("file-menu", contextMenu) } ``` ## Platform Behaviour Context menus are **platform-native**: **Native macOS context menus:** - System animations and transitions - Right-click = Control+Click (automatic) - Adapts to system appearance (light/dark) - Standard text operations in default menu - Native scrolling for long menus **macOS conventions:** - Use sentence case for menu items - Use ellipsis (...) for items that open dialogs - Common shortcuts: โŒ˜C (Copy), โŒ˜V (Paste) **Native Windows context menus:** - Windows native style - Follows Windows theme - Standard Windows operations in default menu - Touch and pen input support **Windows conventions:** - Use title case for menu items - Use ellipsis (...) for items that open dialogs - Common shortcuts: Ctrl+C (Copy), Ctrl+V (Paste) **Desktop environment integration:** - Adapts to desktop theme (GTK, Qt, etc.) - Right-click behaviour follows system settings - Default menu content varies by environment - Positioning follows DE conventions **Linux considerations:** - Test on target desktop environments - GTK and Qt have different behaviours - Some DEs customise context menus ## Complete Example **Go code:** ```go package main import ( "encoding/json" "log" "github.com/wailsapp/wails/v3/pkg/application" ) type FileData struct { ID string `json:"id"` Type string `json:"type"` Name string `json:"name"` } func main() { app := application.New(application.Options{ Name: "Context Menu Demo", }) // Create file context menu fileMenu := createFileMenu(app) app.RegisterContextMenu("file-menu", fileMenu) // Create image context menu imageMenu := createImageMenu(app) app.RegisterContextMenu("image-menu", imageMenu) // Create text context menu textMenu := createTextMenu(app) app.RegisterContextMenu("text-menu", textMenu) app.Window.New() app.Run() } func createFileMenu(app *application.Application) *application.ContextMenu { menu := app.NewContextMenu() menu.Add("Open").OnClick(func(ctx *application.Context) { data := parseFileData(ctx.ContextMenuData()) openFile(data.ID) }) menu.Add("Rename").OnClick(func(ctx *application.Context) { data := parseFileData(ctx.ContextMenuData()) renameFile(data.ID) }) menu.AddSeparator() menu.Add("Delete").OnClick(func(ctx *application.Context) { data := parseFileData(ctx.ContextMenuData()) deleteFile(data.ID) }) return menu } func createImageMenu(app *application.Application) *application.ContextMenu { menu := app.NewContextMenu() menu.Add("View").OnClick(func(ctx *application.Context) { data := parseFileData(ctx.ContextMenuData()) viewImage(data.ID) }) menu.Add("Edit").OnClick(func(ctx *application.Context) { data := parseFileData(ctx.ContextMenuData()) editImage(data.ID) }) menu.AddSeparator() exportMenu := menu.AddSubmenu("Export As") exportMenu.Add("PNG").OnClick(exportPNG) exportMenu.Add("JPEG").OnClick(exportJPEG) exportMenu.Add("WebP").OnClick(exportWebP) return menu } func createTextMenu(app *application.Application) *application.ContextMenu { menu := app.NewContextMenu() menu.Add("Cut").SetAccelerator("CmdOrCtrl+X").OnClick(handleCut) menu.Add("Copy").SetAccelerator("CmdOrCtrl+C").OnClick(handleCopy) menu.Add("Paste").SetAccelerator("CmdOrCtrl+V").OnClick(handlePaste) return menu } func parseFileData(dataStr string) FileData { var data FileData if err := json.Unmarshal([]byte(dataStr), &data); err != nil { log.Printf("Invalid file data: %v", err) } return data } // Handler implementations... func openFile(id string) { /* ... */ } func renameFile(id string) { /* ... */ } func deleteFile(id string) { /* ... */ } func viewImage(id string) { /* ... */ } func editImage(id string) { /* ... */ } func exportPNG(ctx *application.Context) { /* ... */ } func exportJPEG(ctx *application.Context) { /* ... */ } func exportWebP(ctx *application.Context) { /* ... */ } func handleCut(ctx *application.Context) { /* ... */ } func handleCopy(ctx *application.Context) { /* ... */ } func handlePaste(ctx *application.Context) { /* ... */ } ``` **HTML:** ```html

Files

๐Ÿ“„ Report.pdf
๐Ÿ–ผ๏ธ Photo.jpg

Text Editor

No Context Menu

Right-click here - no menu appears
``` ## Best Practices ### โœ… Do - **Keep menus focused** - Only relevant actions for the element - **Validate context data** - Treat as untrusted input - **Use clear labels** - "Delete File" not "Delete" - **Call menu.Update()** - After changing menu state - **Test on all platforms** - Behaviour varies - **Provide keyboard shortcuts** - For common actions - **Group related items** - Use separators ### โŒ Don't - **Don't trust context data** - Always validate - **Don't make menus too long** - 7-10 items maximum - **Don't forget menu.Update()** - Menus won't work properly - **Don't nest too deeply** - 2 levels maximum - **Don't use jargon** - Keep labels user-friendly - **Don't block handlers** - Keep them fast ## Troubleshooting ### Context Menu Not Appearing **Possible causes:** 1. Menu ID mismatch 2. CSS property typo 3. Runtime not initialised **Solution:** ```go // Check menu is registered app.RegisterContextMenu("my-menu", contextMenu) ``` ```html
``` ### Context Data Not Received **Possible causes:** 1. CSS property not set 2. Data contains special characters **Solution:** ```html
``` Or use JavaScript: ```javascript element.style.setProperty('--custom-contextmenu-data', JSON.stringify(data)) ``` ### Menu Items Not Responding **Cause:** Forgot to call `menu.Update()` after enabling **Solution:** ```go menuItem.SetEnabled(true) contextMenu.Update() // Add this! ``` ## Next Steps Complete reference for menu item types and properties. [Learn More โ†’](/features/menus/reference) Create application menu bars. [Learn More โ†’](/features/menus/application) Add system tray/menu bar integration. [Learn More โ†’](/features/menus/systray) Common menu patterns and best practices. [Learn More โ†’](/guides/patterns/menus) --- **Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [context menu example](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/contextmenus).