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

It installs @implementjs/lucide at the same time.

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"),
	),
);

Styled like the dropdown menu

The panels, items, separators, and indicators are drawn to match the dropdown menu, but the classes live in this file — it installs on its own, and editing it restyles the context menu and nothing else.

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.
onOpenChange(open: boolean) => void—Runs whenever the menu opens or closes.
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
disabledReadable<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() => void—Runs when the item is activated with a click, Enter, or Space.
closeOnSelectbooleantrueWhether selecting the item closes the menu.
disabledReadable<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

ContextMenuCheckboxGroup

Wraps checkbox items and owns which of them are checked, as one array of values. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
valueSignal<ItemValue[]> | ItemValue[][]The values of the checked items, where ItemValue is string | number. Pass a signal to control them from outside.
onValueChange(value: ItemValue[]) => void—Runs whenever the set of checked items changes.
Data attributeValue
[data-context-menu-checkbox-group]Present

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
indicatorChild—Drawn in place of the default check. The left padding the default one is absolutely positioned into comes off with it, so a custom indicator sits in the row's flow.
valueItemValue—Identifies the item inside a checkbox group, as a string or a number. Must be unique within the group; ignored outside one.
checkedSignal<boolean> | booleanfalseThe checked state; selecting toggles it. Pass a signal to control it from outside. Inside a checkbox group the group's value owns it instead.
onCheckedChange(checked: boolean) => void—Runs whenever the item's checked state changes, inside a group or on its own.
closeOnSelectbooleantrueWhether selecting the item closes the menu.
disabledReadable<boolean> | booleanfalsePrevents selecting the item; the keyboard skips it.
Data attributeValue
[data-context-menu-checkbox-item]Present
[data-value]The item's value, when it has one
[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<ItemValue | null> | ItemValue | nullnullThe checked item, where ItemValue is string | number. Pass a signal to control it from outside.
onValueChange(value: ItemValue | null) => void—Runs whenever the checked item changes. null once nothing is checked.
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*ItemValue—Identifies the item, as a string or a number. Must be unique within the radio group.
closeOnSelectbooleantrueWhether selecting the item closes the menu.
disabledReadable<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.
onOpenChange(open: boolean) => void—Runs whenever the submenu opens or closes.

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