implement
Context Menu

Context Menu

A menu opened by right-clicking a region of the page.

Right click here
import {
	ContextMenu,
	ContextMenuContent,
	ContextMenuItem,
	ContextMenuSeparator,
	ContextMenuSub,
	ContextMenuSubContent,
	ContextMenuSubTrigger,
	ContextMenuTrigger,
} from "@/lib/components/ui/context-menu";

export default function ContextMenuDemo() {
	return ContextMenu(
		ContextMenuTrigger(
			{
				class:
					"flex h-36 w-full max-w-sm items-center justify-center rounded-md border border-dashed text-sm select-none",
			},
			"Right click here",
		),
		ContextMenuContent(
			{ class: "w-52" },
			ContextMenuItem({ onSelect: () => console.log("back") }, "Back"),
			ContextMenuItem({ disabled: true }, "Forward"),
			ContextMenuItem({ onSelect: () => console.log("reload") }, "Reload"),
			ContextMenuSeparator(),
			ContextMenuSub(
				ContextMenuSubTrigger("Share"),
				ContextMenuSubContent(
					{ class: "w-44" },
					ContextMenuItem({ onSelect: () => console.log("share-email") }, "Email"),
					ContextMenuItem({ onSelect: () => console.log("share-message") }, "Message"),
					ContextMenuItem({ onSelect: () => console.log("share-link") }, "Copy link"),
				),
			),
			ContextMenuSeparator(),
			ContextMenuItem({ onSelect: () => console.log("save") }, "Save page as…"),
			ContextMenuItem({ onSelect: () => console.log("print") }, "Print…"),
			ContextMenuSeparator(),
			ContextMenuItem({ onSelect: () => console.log("inspect") }, "Inspect"),
		),
	);
}

Installation

npx jsrepo add @implementjs/ui/context-menu

jsrepo pulls dropdown-menu along with it.

Usage

The same menu as the dropdown, anchored to the pointer instead of a trigger button. ContextMenuTrigger is the region you right-click, and it is passed straight through from the primitive — it has no styling of its own, so the region is yours to define.

import {
	ContextMenu,
	ContextMenuContent,
	ContextMenuItem,
	ContextMenuSeparator,
	ContextMenuTrigger,
} from "@/lib/components/ui/context-menu";

ContextMenu(
	ContextMenuTrigger({ class: "rounded-md border border-dashed p-8" }, "Right click here"),
	ContextMenuContent(
		{ class: "w-52" },
		ContextMenuItem({ onSelect: back }, "Back"),
		ContextMenuItem({ disabled: true }, "Forward"),
		ContextMenuSeparator(),
		ContextMenuItem({ onSelect: inspect }, "Inspect"),
	),
);

Shared with the dropdown menu

Every panel and item class comes from dropdown-menu.tsmenuContentClasses, menuItemClasses, and the check and chevron indicators are exported from there and used here, by the menubar, and by the select's group headings. Restyling the menus is one file, not four.

That is also why installing this one brings dropdown-menu with it.

API Reference

Every prop the styling does not consume is forwarded to the Context Menu primitive, so the tables below are the whole surface — the behavior props and the styling ones together.

ContextMenu

The root. Owns whether the menu is open and provides that to the parts inside it.

PropTypeDefaultDescription
openSignal<boolean> | booleanfalseThe open state. Pass a signal to control it from outside; a boolean seeds uncontrolled state.
preventScrollbooleantrueWhen true, the page behind cannot scroll while the menu is open. The panel can still scroll if you give it overflow.

ContextMenuTrigger

The area the menu belongs to. Right-clicking (or long-pressing on touch) opens the menu at the pointer. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
disabledSignal<boolean> | booleanfalseFalls back to the native browser menu.
Data attributeValue
[data-context-menu-trigger]Present
[data-state]"open" | "closed"
[data-disabled]Present when disabled

ContextMenuContent

The floating panel of items. Sets role="menu". Styled as a popover panel that fades and scales in from the side it opens on, and hides itself when closed. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
side"top" | "bottom" | "left" | "right""bottom"Preferred side of the anchor to place the panel.
align"start" | "center" | "end""start"How the panel aligns along the chosen side.
offsetnumber0Distance in pixels between the anchor and the panel.
loopbooleantrueWhether arrow keys wrap from the last item back to the first.
Data attributeValue
[data-context-menu-content]Present
[data-state]"open" | "closed"
[data-side]"top" | "bottom" | "left" | "right"
[data-align]"start" | "center" | "end"

ContextMenuItem

One action. Sets role="menuitem"; focus follows the pointer. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
onSelect() => voidRuns when the item is activated with a click, Enter, or Space.
closeOnSelectbooleantrueWhether selecting the item closes the menu.
disabledSignal<boolean> | booleanfalsePrevents selecting the item; the keyboard skips it.
Data attributeValue
[data-context-menu-item]Present
[data-highlighted]Present while focused
[data-disabled]Present when disabled

ContextMenuCheckboxItem

An item holding a checked state. Sets role="menuitemcheckbox" and aria-checked. Renders its own check indicator, shown while the item is checked. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
checkedSignal<boolean> | booleanfalseThe checked state; selecting toggles it. Pass a signal to control it from outside.
closeOnSelectbooleantrueWhether selecting the item closes the menu.
disabledSignal<boolean> | booleanfalsePrevents selecting the item; the keyboard skips it.
Data attributeValue
[data-context-menu-checkbox-item]Present
[data-state]"checked" | "unchecked"
[data-highlighted]Present while focused
[data-disabled]Present when disabled

ContextMenuRadioGroup

Wraps radio items and owns which one is checked. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
valueSignal<string | null> | string | nullnullThe checked item. Pass a signal to control it from outside.
Data attributeValue
[data-context-menu-radio-group]Present

ContextMenuRadioItem

One choice in a radio group. Sets role="menuitemradio" and aria-checked. Renders its own dot indicator, shown while the item is selected. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
value*stringIdentifies the item. Must be unique within the radio group.
closeOnSelectbooleantrueWhether selecting the item closes the menu.
disabledSignal<boolean> | booleanfalsePrevents selecting the item; the keyboard skips it.
Data attributeValue
[data-context-menu-radio-item]Present
[data-state]"checked" | "unchecked"
[data-highlighted]Present while focused
[data-disabled]Present when disabled

ContextMenuSub

A nested menu. Wraps a sub trigger and its sub content inside a parent content.

PropTypeDefaultDescription
openSignal<boolean> | booleanfalseThe open state. Pass a signal to control it from outside; a boolean seeds uncontrolled state.

ContextMenuSubTrigger

The item that opens its submenu — on hover after openDelay, ArrowRight, Enter, Space, or click. Sets aria-haspopup and aria-expanded. The styled trigger appends a chevron pointing into the submenu. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
openDelaynumber100How long the pointer must rest on the trigger before the submenu opens, in milliseconds.
disabledSignal<boolean> | booleanfalsePrevents opening the submenu; the keyboard skips the item.
Data attributeValue
[data-context-menu-sub-trigger]Present
[data-context-menu-item]Present
[data-state]"open" | "closed"
[data-highlighted]Present while focused
[data-disabled]Present when disabled

ContextMenuSubContent

A submenu's panel. Styled like the content panel but without the transition — a submenu should feel instant. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
side"top" | "bottom" | "left" | "right""right"Preferred side of the sub trigger to place the panel.
align"start" | "center" | "end""start"How the panel aligns along the chosen side.
offsetnumber8Distance in pixels between the sub trigger and the panel.
loopbooleantrueWhether arrow keys wrap from the last item back to the first.
Data attributeValue
[data-context-menu-sub-content]Present
[data-state]"open" | "closed"
[data-side]"top" | "bottom" | "left" | "right"
[data-align]"start" | "center" | "end"

ContextMenuGroup

Wraps related items in role="group", labeled by the heading placed inside it. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-context-menu-group]Present

ContextMenuGroupHeading

Names the group it sits in; the group points aria-labelledby at it. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-context-menu-group-heading]Present

ContextMenuSeparator

A role="separator" line between sections. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-context-menu-separator]Present

ContextMenuPortal

The core Portal helper, for rendering the panel into another DOM parent to escape overflow and stacking.