Context Menu

Nib supports two kinds of context menus:

  1. Status bar menu (app.menu): appears when the user right-clicks the menu bar icon. Built with MenuItem and MenuDivider objects.
  2. View context menu (context_menu=): appears when the user right-clicks any view in your app. Built with nib views like Button, Toggle, Picker, Divider, Text, and ShareLink.

Status Bar Menu

The status bar context menu appears when a user right-clicks (or Control-clicks) the menu bar icon. Set it with app.menu, passing a list of MenuItem and MenuDivider objects.

Context menu with custom items and keyboard shortcuts

Basic Menu

import nib

def main(app: nib.App):
    app.title = "My App"
    app.icon = nib.SFSymbol("star.fill")
    app.width = 300
    app.height = 200

    def open_settings():
        print("Settings opened")

    app.menu = [
        nib.MenuItem("Settings", action=open_settings, icon="gear"),
        nib.MenuDivider(),
        nib.MenuItem("Quit", action=app.quit),
    ]

    app.build(nib.Text("Right-click the menu bar icon", padding=24))

nib.run(main)

Icons

Pass a string for a simple SF Symbol icon, or an SFSymbol instance for styled icons:

# Simple string icon
nib.MenuItem("Settings", action=handler, icon="gear")

# Styled icon with color
nib.MenuItem(
    "Favorites",
    action=handler,
    icon=nib.SFSymbol("star.fill", foreground_color=nib.Color.YELLOW),
)

Keyboard Shortcuts

Use the shortcut parameter with modifier keys joined by +:

nib.MenuItem("Settings", action=open_settings, shortcut="cmd+,")
nib.MenuItem("New Window", action=new_window, shortcut="cmd+n")
nib.MenuItem("Quit", action=app.quit, shortcut="cmd+q")
nib.MenuItem("Refresh", action=refresh, shortcut="cmd+shift+r")
nib.MenuItem("Toggle", action=toggle, shortcut="opt+t")

Supported modifiers: cmd, shift, opt (or alt), ctrl.

Badges

Badges display a small label on the right side of the menu item. They are useful for showing counts or status.

nib.MenuItem("Updates", action=check_updates, badge="3")
nib.MenuItem("Messages", action=open_messages, badge="New")

State Indicators

The state parameter shows a checkmark, no mark, or a mixed indicator:

nib.MenuItem("Enabled", action=toggle_feature, state="on")     # Checkmark
nib.MenuItem("Disabled", action=toggle_feature, state="off")    # No mark
nib.MenuItem("Partial", action=toggle_feature, state="mixed")   # Dash mark

A common pattern is to toggle the state dynamically:

feature_on = False

def toggle_feature():
    global feature_on
    feature_on = not feature_on
    rebuild_menu()

def rebuild_menu():
    app.menu = [
        nib.MenuItem(
            "Feature",
            action=toggle_feature,
            state="on" if feature_on else "off",
            icon="checkmark.circle",
        ),
        nib.MenuDivider(),
        nib.MenuItem("Quit", action=app.quit),
    ]

Disabled Items

Set enabled=False to gray out an item and prevent clicks:

nib.MenuItem("Unavailable", enabled=False)
nib.MenuItem("Premium Only", action=upgrade, enabled=is_premium)

MenuDivider draws a horizontal separator line between items:

app.menu = [
    nib.MenuItem("Action 1", action=handler1),
    nib.MenuItem("Action 2", action=handler2),
    nib.MenuDivider(),
    nib.MenuItem("Quit", action=app.quit),
]

Nest menus by passing a menu list to a MenuItem:

nib.MenuItem(
    "Export",
    icon="square.and.arrow.up",
    menu=[
        nib.MenuItem("Export as PDF", action=export_pdf, icon="doc"),
        nib.MenuItem("Export as PNG", action=export_png, icon="photo"),
        nib.MenuItem("Export as CSV", action=export_csv, icon="tablecells"),
    ],
)

Submenus can be nested to any depth:

nib.MenuItem(
    "More",
    icon="ellipsis.circle",
    menu=[
        nib.MenuItem("Level 1", menu=[
            nib.MenuItem("Level 2 - A", action=handler_a),
            nib.MenuItem("Level 2 - B", action=handler_b),
        ]),
    ],
)

Custom View Content

For rich menu items, use the content parameter with any Nib view. When using content, the title and icon parameters are ignored.

nib.MenuItem(
    content=nib.HStack(
        controls=[
            nib.SFSymbol("star.fill", foreground_color=nib.Color.YELLOW),
            nib.VStack(
                controls=[
                    nib.Text("Premium", font=nib.Font.HEADLINE),
                    nib.Text("Upgrade now", font=nib.Font.CAPTION,
                              foreground_color=nib.Color.SECONDARY),
                ],
                alignment=nib.HorizontalAlignment.LEADING,
                spacing=2,
            ),
        ],
        spacing=8,
    ),
    action=upgrade,
    height=50,
)

Tip

Set the height parameter when using content to ensure the menu item has enough vertical space for your custom view.

View Context Menu

View context menu

Any nib view supports a context_menu parameter. When set, right-clicking that view shows a native macOS context menu with the specified items. This uses SwiftUI's .contextMenu modifier under the hood.

Basic Usage

nib.Text(
    "Right-click me",
    context_menu=[
        nib.Button("Copy", action=copy_fn),
        nib.Button("Paste", action=paste_fn),
        nib.Divider(),
        nib.Button("Delete", action=delete_fn, role="destructive"),
    ],
    padding=20,
)

The context_menu parameter accepts a list of nib views. It works on any view. Text, VStack, Image, Button, etc.

Supported Menu Item Views

SwiftUI context menus support a specific set of views as menu items:

View Description
Button Clickable action item. Supports role="destructive" for red delete-style items.
Button with content= Custom button layout with icon + text using HStack.
Divider Horizontal separator between groups of items.
Toggle On/off switch inside the menu.
Picker Inline selection from a list of options.
Text Static, non-interactive label (e.g., version info).
ShareLink Opens the native macOS share sheet.

Note

Layout views like VStack, HStack, Image, and List are not rendered inside context menus. SwiftUI only supports menu-compatible views in the .contextMenu builder.

Button Variants

Buttons are the most common menu item. You can create plain buttons, icon buttons, and destructive buttons:

context_menu=[
    # Plain text button
    nib.Button("Copy", action=copy_fn),

    # Button with icon
    nib.Button(
        content=nib.HStack(
            controls=[nib.SFSymbol("doc.on.doc"), nib.Text("Duplicate")]
        ),
        action=duplicate_fn,
    ),

    # Destructive button (shown in red)
    nib.Button("Delete", action=delete_fn, role="destructive"),
]

Toggle and Picker

Interactive controls work inside context menus:

dark_mode = False

def toggle_dark(value):
    nonlocal dark_mode
    dark_mode = value

def on_pick(value):
    print(f"Selected: {value}")

context_menu=[
    nib.Toggle("Dark Mode", is_on=dark_mode, on_change=toggle_dark),
    nib.Picker(
        "Size",
        selection="Medium",
        options=["Small", "Medium", "Large"],
        on_change=on_pick,
    ),
]

Works on Any View

Since context_menu is a base View parameter, it works on containers, controls, shapes. Anything:

# On a container
nib.VStack(
    controls=[nib.Text("Content")],
    context_menu=[nib.Button("Refresh", action=refresh)],
    padding=16,
)

# On an image
nib.Image(
    "photo.jpg",
    context_menu=[
        nib.Button("Save", action=save),
        nib.Button("Copy", action=copy),
        nib.ShareLink(items=["photo.jpg"], label="Share"),
    ],
)