---
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).
