gui/docs/ref/wails-v3/features/menus/context.mdx
Snider 4bdbb68f46
Some checks failed
Security Scan / security (push) Failing after 9s
Test / test (push) Failing after 1m21s
refactor: update import path from go-config to core/config
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-14 10:26:36 +00:00

728 lines
17 KiB
Text

---
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
<textarea style="--custom-contextmenu: editor-menu">
Right-click me!
</textarea>
```
**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
<div style="--custom-contextmenu: menu-id">
Right-click me!
</div>
```
**CSS property:** `--custom-contextmenu: <menu-id>`
### With Context Data
Pass data from HTML to Go:
```html
<div style="--custom-contextmenu: file-menu; --custom-contextmenu-data: file-123">
Right-click this file
</div>
```
**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: <menu-id>` - Which menu to show
- `--custom-contextmenu-data: <data>` - Data to pass to handlers
### Dynamic Data
Generate data dynamically in JavaScript:
```html
<div id="file-item" style="--custom-contextmenu: file-menu">
File.txt
</div>
<script>
// Set data dynamically
const fileItem = document.getElementById('file-item')
fileItem.style.setProperty('--custom-contextmenu-data', 'file-' + fileId)
</script>
```
### Multiple Elements, Same Menu
```html
<div class="file-item" style="--custom-contextmenu: file-menu; --custom-contextmenu-data: file-1">
Document.pdf
</div>
<div class="file-item" style="--custom-contextmenu: file-menu; --custom-contextmenu-data: file-2">
Image.png
</div>
<div class="file-item" style="--custom-contextmenu: file-menu; --custom-contextmenu-data: file-3">
Video.mp4
</div>
```
**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
<div style="--custom-contextmenu: item-menu; --custom-contextmenu-data: {&quot;id&quot;:123,&quot;type&quot;:&quot;image&quot;}">
Image.png
</div>
```
**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
<div style="--default-contextmenu: hide">
No default menu here
</div>
```
**Use case:** Custom UI elements where default menu doesn't make sense.
### Show Default Menu
```html
<div style="--default-contextmenu: show">
Default menu always shown
</div>
```
**Use case:** Text areas, input fields, editable content.
### Auto (Smart) Mode
```html
<div style="--default-contextmenu: auto">
Smart context menu
</div>
```
**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
<!-- Custom menu + default menu -->
<textarea style="--custom-contextmenu: editor-menu; --default-contextmenu: show">
Both menus available
</textarea>
```
**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**:
<Tabs syncKey="platform">
<TabItem label="macOS" icon="apple">
**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)
</TabItem>
<TabItem label="Windows" icon="seti:windows">
**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)
</TabItem>
<TabItem label="Linux" icon="linux">
**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
</TabItem>
</Tabs>
## 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
<!DOCTYPE html>
<html>
<head>
<style>
.file-item {
padding: 10px;
margin: 5px;
border: 1px solid #ccc;
cursor: pointer;
}
.file-item:hover {
background: #f0f0f0;
}
textarea {
width: 100%;
height: 200px;
}
</style>
</head>
<body>
<h2>Files</h2>
<!-- Regular file -->
<div class="file-item"
style="--custom-contextmenu: file-menu;
--custom-contextmenu-data: {&quot;id&quot;:&quot;file-1&quot;,&quot;type&quot;:&quot;document&quot;,&quot;name&quot;:&quot;Report.pdf&quot;}">
📄 Report.pdf
</div>
<!-- Image file -->
<div class="file-item"
style="--custom-contextmenu: image-menu;
--custom-contextmenu-data: {&quot;id&quot;:&quot;file-2&quot;,&quot;type&quot;:&quot;image&quot;,&quot;name&quot;:&quot;Photo.jpg&quot;}">
🖼️ Photo.jpg
</div>
<h2>Text Editor</h2>
<!-- Text area with custom menu + default menu -->
<textarea
style="--custom-contextmenu: text-menu; --default-contextmenu: show"
placeholder="Type here, then right-click...">
</textarea>
<h2>No Context Menu</h2>
<!-- Disable default menu -->
<div style="--default-contextmenu: hide; padding: 20px; border: 1px solid #ccc;">
Right-click here - no menu appears
</div>
</body>
</html>
```
## 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
<!-- Check ID matches -->
<div style="--custom-contextmenu: my-menu">
```
### Context Data Not Received
**Possible causes:**
1. CSS property not set
2. Data contains special characters
**Solution:**
```html
<!-- Escape quotes in JSON -->
<div style="--custom-contextmenu-data: {&quot;id&quot;:123}">
```
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
<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="System Tray Menus" icon="star">
Add system tray/menu bar integration.
[Learn More →](/features/menus/systray)
</Card>
<Card title="Menu Patterns" icon="open-book">
Common menu patterns and best practices.
[Learn More →](/guides/patterns/menus)
</Card>
</CardGrid>
---
**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).