Context Menu
A menu that appears at the pointer on right click or long press.
Last action: none · Bookmarks on · Person pedro
Codeapp/demos/public/context-menu.tsx
import { clientEntry, type Handle } from 'remix/component'
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
onMenuSelect,
} from '@stemsui/registry/ui/context-menu.tsx'
export const ContextMenuDemo = clientEntry(
import.meta.url,
function ContextMenuDemo(handle: Handle) {
let lastAction = 'none'
let showBookmarks = true
let showUrls = false
let person = 'pedro'
return () => (
<div
class="flex flex-col items-center gap-4"
mix={onMenuSelect((event) => {
if (event.kind === 'item') {
lastAction = String(event.value ?? event.label)
void handle.update()
}
})}
>
<ContextMenu>
<ContextMenuTrigger
tabIndex={0}
data-testid="context-area"
class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
Right click here
</ContextMenuTrigger>
<ContextMenuContent class="w-52">
<ContextMenuGroup>
<ContextMenuItem value="back">
Back
<ContextMenuShortcut>⌘[</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem value="forward" disabled>
Forward
<ContextMenuShortcut>⌘]</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem value="reload">
Reload
<ContextMenuShortcut>⌘R</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger>More Tools</ContextMenuSubTrigger>
<ContextMenuSubContent class="w-44">
<ContextMenuItem value="save-page">Save Page...</ContextMenuItem>
<ContextMenuItem value="create-shortcut">Create Shortcut...</ContextMenuItem>
<ContextMenuItem value="name-window">Name Window...</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem value="developer-tools">Developer Tools</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem value="delete" variant="destructive">
Delete
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuCheckboxItem
checked={showBookmarks}
onCheckedChange={(checked) => {
showBookmarks = checked
void handle.update()
}}
>
Show Bookmarks
</ContextMenuCheckboxItem>
<ContextMenuCheckboxItem
checked={showUrls}
onCheckedChange={(checked) => {
showUrls = checked
void handle.update()
}}
>
Show Full URLs
</ContextMenuCheckboxItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuRadioGroup
value={person}
onValueChange={(value) => {
person = String(value)
void handle.update()
}}
>
<ContextMenuLabel>People</ContextMenuLabel>
<ContextMenuRadioItem value="pedro">Pedro Duarte</ContextMenuRadioItem>
<ContextMenuRadioItem value="colm">Colm Tuite</ContextMenuRadioItem>
</ContextMenuRadioGroup>
</ContextMenuContent>
</ContextMenu>
<p class="text-sm text-muted-foreground" data-testid="context-menu-result">
Last action: <span class="font-medium text-foreground">{lastAction}</span> · Bookmarks{' '}
{showBookmarks ? 'on' : 'off'} · Person {person}
</p>
</div>
)
},
)
Installation
npx shadcn add @stemsui/context-menuRequires the one-time setup. Behavior: stems-ui/context-menu.
API reference
The styled component is built on the headless module below: its props mirror these options, and the same events and data attributes apply.
stems-ui/context-menu
context-menu opens a menu at the pointer when an area is right-clicked or long-pressed. It is
stems-ui/menu with a different root and trigger: every item, group, and submenu part is the
same.
Base UI equivalent: Context Menu.
Usage
import * as contextMenu from 'stems-ui/context-menu'
function FileRow() {
return () => (
<contextMenu.Context>
<div tabIndex={0} class="select-none" mix={contextMenu.trigger()}>
report.pdf
</div>
<div class="rounded-md bg-white p-1 shadow-md" mix={contextMenu.popup()}>
<div mix={contextMenu.item({ value: 'rename' })}>Rename</div>
<div mix={contextMenu.item({ value: 'delete' })}>Delete</div>
</div>
</contextMenu.Context>
)
}
Module split
Base UI exports ContextMenu.Root + ContextMenu.Trigger plus all Menu parts under the
ContextMenu namespace, so Stems does the same: stems-ui/context-menu exports its own
Context and trigger() and re-exports every stems-ui/menu part (popup, item,
checkboxItem, RadioGroupContext, SubmenuContext, submenuTrigger, …). Mixing parts
from either module works, since both share one implementation.
Differences from Base UI
- Same as menu (no Portal / Positioner, item providers, no filtering).
- Keyboard opening (Shift+F10, the ContextMenu key) is a Stems addition: the menu opens at the area's bottom-left corner with the first item focused.
modaldefaults tofalseso a right click elsewhere in the area repositions the open menu. Opening a context menu closes any other open context menu.- A right click outside every trigger area doesn't dismiss the menu, because the dismiss layer only handles left-button presses. The browser's native menu opens on top instead.
stems-ui/context-menu
Context(Base UIContextMenu.Root): same props asmenu.Context(open/defaultOpen/onOpenChange,onOpenChangeComplete,loopFocus,orientation,disabled,highlightItemOnHover,onItemHighlighted), withmodaldefaulting tofalse.trigger()(Base UIContextMenu.Trigger): the area. No ARIA is added (make it focusable withtabIndex={0}for keyboard access).contextmenu(right click):preventDefault(), opens at{ clientX, clientY }with the popup focused. Right-clicking again while open repositions it.- Touch long press (500ms, cancelled by moving more than 10px or lifting): opens at the touch
point. The
touchendthat follows is canceled so the compatibility click doesn't close it. - Shift+F10 / ContextMenu key: opens at the area's bottom-left corner, focusing the first item.
popup(options?): as in menu, but positioned against the pointer. Defaults toside: 'bottom',align: 'start'. The native context menu is suppressed inside the popup.- Every other part: see menu.
ContextMenuChangeEvent(stems:context-menu-change,onContextMenuChange(handler)): bubbles from the popup withopenandreason('trigger-press'when opened).MenuSelectEvent/onMenuSelectas in menu.
Behavior notes
- Focus: when the menu closes, focus returns to the element that was focused when it opened
(
finalFocusoverrides this). - Keyboard, typeahead, submenus, activation, and dismissal work as in menu.
Styling hooks
| Part | Attribute | Values / notes |
|---|---|---|
| trigger | data-popup-open, data-pressed | present while the menu is open |
| popup … | see menu | data-side / data-align vs. the point |