Context Menu
Declare a real OS context menu with useContextMenu — no HTML popup involved.
Murasaki's context menu is declared with a hook, not markup, so it lives right
next to your state — its actions can close over useState setters. None of
it renders HTML: items are posted to the Rust side, which pops a real OS menu
(NSMenu on macOS and HMENU on Windows). Grant menu:context to each
renderer that declares one:
window: { capabilities: ['menu:context', 'clipboard:writeText'] }Native roles require their own capability as well. The example grants
clipboard:writeText for <Action.Copy />; a plain custom callback needs only
menu:context.
The whole-window menu
Calling useContextMenu with no id declares the default menu — it opens
anywhere the user right-clicks that isn't claimed by a scoped menu:
import { useContextMenu, Action } from 'murasaki'
useContextMenu([
{ label: 'Reload', shortcut: 'command,R', action: <Action.Reload /> },
{ separator: true },
{ label: 'Copy', action: <Action.Copy /> },
])Scoped menus
Give the menu an id and tag a region with a matching <ContextMenuTrigger> —
only that region opens it, and it takes priority over the window default:
import { useContextMenu, ContextMenuTrigger } from 'murasaki'
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
useContextMenu('card', [
{ label: 'Increment', action: () => setCount((n) => n + 1) },
])
return (
<ContextMenuTrigger id="card">
<div>Clicked {count} times</div>
</ContextMenuTrigger>
)
}<ContextMenuTrigger> clones its child by default when it has exactly one
element child (no wrapper node); pass asChild={false} to force a
display: contents <span> wrapper instead (e.g. with multiple children).
Item shape
Each entry is either a divider or:
interface ContextMenuEntry {
label: string
shortcut?: string // e.g. "command,I" — also wired to keydown directly
disabled?: boolean
action?: ContextMenuAction // a built-in <Action.*/> element, or a function
items?: ContextMenuItemSpec[] // a submenu, instead of an action
}
type ContextMenuItemSpec = ContextMenuEntry | { separator: true }shortcut both sets the menu's native accelerator label and registers a
keydown handler, so it fires without the menu being open.
Actions
action is either your own function, or one of the built-in <Action.*/>
elements — native OS roles (Copy, Paste, Cut, SelectAll, Undo,
Redo, Quit) or client behaviors (Reload, Navigate, Run). See
Native APIs for the full
list.
Every native context menu requires menu:context. Native role actions also
require the same permission as their direct native API:
| Action | Additional capability |
|---|---|
Copy, Cut | clipboard:writeText |
Paste | clipboard:readText |
Quit | app:quit |
SelectAll, Undo, Redo | None beyond menu:context |
Murasaki rejects the complete menu payload when a required role capability is missing. Client behaviors and your own callbacks need no additional native capability.
Reusable actions with createActions
Define your app's actions once — typically in src/lib/action.ts, backed by a
store so they're callable from anywhere — and drop them into any menu as
<Action.name />:
import { createActions } from 'murasaki'
import { useCounter } from './counter'
export const Action = createActions({
increment: () => useCounter.getState().increment(),
reset: () => useCounter.getState().reset(),
})import { Action } from '@/lib/action'
useContextMenu('card', [
{ label: 'Increment', shortcut: 'command,I', action: <Action.increment /> },
{ label: 'Reset counter', action: <Action.reset /> },
])The object createActions returns also includes the built-ins (Action.Copy,
Action.Reload, …), so a file can import a single Action for everything.
Only one whole-window (useContextMenu(items), no id) menu should be
mounted at a time — mounting a second one logs a dev warning, and the last
one mounted wins.