Context Menu
A menu opened by right-clicking a region of the page.
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.
Copy the file below to src/lib/components/ui/context-menu.ts. It imports cn from utils.ts, which belongs at src/lib/utils.ts, and dropdown-menu from the same directory — copy those in beside it too.
import { type Child, type ComponentProps } from "@implementjs/core";
import {
ContextMenu as ContextMenuPrimitive,
ContextMenuCheckboxItem as ContextMenuCheckboxItemPrimitive,
ContextMenuContent as ContextMenuContentPrimitive,
ContextMenuGroup as ContextMenuGroupPrimitive,
ContextMenuGroupHeading as ContextMenuGroupHeadingPrimitive,
ContextMenuItem as ContextMenuItemPrimitive,
ContextMenuRadioGroup as ContextMenuRadioGroupPrimitive,
ContextMenuRadioItem as ContextMenuRadioItemPrimitive,
ContextMenuSeparator as ContextMenuSeparatorPrimitive,
ContextMenuSub as ContextMenuSubPrimitive,
ContextMenuSubContent as ContextMenuSubContentPrimitive,
ContextMenuSubTrigger as ContextMenuSubTriggerPrimitive,
ContextMenuTrigger as ContextMenuTriggerPrimitive,
} from "@implementjs/primitives";
import {
MenuCheckIndicator,
MenuRadioIndicator,
MenuSubTriggerChevron,
menuContentClasses,
menuGroupHeadingClasses,
menuIndicatorItemClasses,
menuItemClasses,
menuSeparatorClasses,
menuStaticPanelClasses,
menuSubTriggerClasses,
} from "./dropdown-menu";
import { createComponent } from "@implementjs/primitives";
import { cn } from "@/lib/utils";
export type ContextMenuProps = ComponentProps<typeof ContextMenuPrimitive>;
export const ContextMenu = ContextMenuPrimitive;
export type ContextMenuTriggerProps = ComponentProps<typeof ContextMenuTriggerPrimitive>;
export const ContextMenuTrigger = ContextMenuTriggerPrimitive;
export type ContextMenuContentProps = ComponentProps<typeof ContextMenuContentPrimitive>;
export const ContextMenuContent = createComponent(function ContextMenuContent(
{ class: className, ...props }: ContextMenuContentProps,
...children: Child[]
) {
return ContextMenuContentPrimitive(
{
...props,
"data-slot": "context-menu-content",
class: cn(
menuContentClasses,
"origin-(--ip-context-menu-content-transform-origin)",
className,
),
},
...children,
);
});
export type ContextMenuItemProps = ComponentProps<typeof ContextMenuItemPrimitive>;
export const ContextMenuItem = createComponent(function ContextMenuItem(
{ class: className, ...props }: ContextMenuItemProps,
...children: Child[]
) {
return ContextMenuItemPrimitive(
{ ...props, "data-slot": "context-menu-item", class: cn(menuItemClasses, className) },
...children,
);
});
export type ContextMenuCheckboxItemProps = ComponentProps<typeof ContextMenuCheckboxItemPrimitive>;
export const ContextMenuCheckboxItem = createComponent(function ContextMenuCheckboxItem(
{ class: className, ...props }: ContextMenuCheckboxItemProps,
...children: Child[]
) {
return ContextMenuCheckboxItemPrimitive(
{
...props,
"data-slot": "context-menu-checkbox-item",
class: cn("group/menu-item", menuItemClasses, menuIndicatorItemClasses, className),
},
MenuCheckIndicator(),
...children,
);
});
export type ContextMenuRadioGroupProps = ComponentProps<typeof ContextMenuRadioGroupPrimitive>;
export const ContextMenuRadioGroup = ContextMenuRadioGroupPrimitive;
export type ContextMenuRadioItemProps = ComponentProps<typeof ContextMenuRadioItemPrimitive>;
export const ContextMenuRadioItem = createComponent(function ContextMenuRadioItem(
{ class: className, ...props }: ContextMenuRadioItemProps,
...children: Child[]
) {
return ContextMenuRadioItemPrimitive(
{
...props,
"data-slot": "context-menu-radio-item",
class: cn("group/menu-item", menuItemClasses, menuIndicatorItemClasses, className),
},
MenuRadioIndicator(),
...children,
);
});
export type ContextMenuGroupProps = ComponentProps<typeof ContextMenuGroupPrimitive>;
export const ContextMenuGroup = ContextMenuGroupPrimitive;
export type ContextMenuGroupHeadingProps = ComponentProps<typeof ContextMenuGroupHeadingPrimitive>;
export const ContextMenuGroupHeading = createComponent(function ContextMenuGroupHeading(
{ class: className, ...props }: ContextMenuGroupHeadingProps,
...children: Child[]
) {
return ContextMenuGroupHeadingPrimitive(
{
...props,
"data-slot": "context-menu-group-heading",
class: cn(menuGroupHeadingClasses, className),
},
...children,
);
});
export type ContextMenuSeparatorProps = ComponentProps<typeof ContextMenuSeparatorPrimitive>;
export const ContextMenuSeparator = createComponent(function ContextMenuSeparator({
class: className,
...props
}: ContextMenuSeparatorProps) {
return ContextMenuSeparatorPrimitive({
...props,
"data-slot": "context-menu-separator",
class: cn(menuSeparatorClasses, className),
});
});
export type ContextMenuSubProps = ComponentProps<typeof ContextMenuSubPrimitive>;
export const ContextMenuSub = ContextMenuSubPrimitive;
export type ContextMenuSubTriggerProps = ComponentProps<typeof ContextMenuSubTriggerPrimitive>;
export const ContextMenuSubTrigger = createComponent(function ContextMenuSubTrigger(
{ class: className, ...props }: ContextMenuSubTriggerProps,
...children: Child[]
) {
return ContextMenuSubTriggerPrimitive(
{
...props,
"data-slot": "context-menu-sub-trigger",
class: cn(menuItemClasses, menuSubTriggerClasses, className),
},
...children,
MenuSubTriggerChevron(),
);
});
export type ContextMenuSubContentProps = ComponentProps<typeof ContextMenuSubContentPrimitive>;
export const ContextMenuSubContent = createComponent(function ContextMenuSubContent(
{ class: className, offset = 8, ...props }: ContextMenuSubContentProps,
...children: Child[]
) {
return ContextMenuSubContentPrimitive(
{
offset,
...props,
"data-slot": "context-menu-sub-content",
class: cn(menuStaticPanelClasses, className),
},
...children,
);
});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.ts — menuContentClasses, 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.
| Prop | Type | Default | Description |
|---|---|---|---|
open | Signal<boolean> | boolean | false | The open state. Pass a signal to control it from outside; a boolean seeds uncontrolled state. |
preventScroll | boolean | true | When 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.
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | Signal<boolean> | boolean | false | Falls back to the native browser menu. |
| Data attribute | Value |
|---|---|
[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.
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
offset | number | 0 | Distance in pixels between the anchor and the panel. |
loop | boolean | true | Whether arrow keys wrap from the last item back to the first. |
| Data attribute | Value |
|---|---|
[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.
| Prop | Type | Default | Description |
|---|---|---|---|
onSelect | () => void | — | Runs when the item is activated with a click, Enter, or Space. |
closeOnSelect | boolean | true | Whether selecting the item closes the menu. |
disabled | Signal<boolean> | boolean | false | Prevents selecting the item; the keyboard skips it. |
| Data attribute | Value |
|---|---|
[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.
| Prop | Type | Default | Description |
|---|---|---|---|
checked | Signal<boolean> | boolean | false | The checked state; selecting toggles it. Pass a signal to control it from outside. |
closeOnSelect | boolean | true | Whether selecting the item closes the menu. |
disabled | Signal<boolean> | boolean | false | Prevents selecting the item; the keyboard skips it. |
| Data attribute | Value |
|---|---|
[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.
| Prop | Type | Default | Description |
|---|---|---|---|
value | Signal<string | null> | string | null | null | The checked item. Pass a signal to control it from outside. |
| Data attribute | Value |
|---|---|
[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.
| Prop | Type | Default | Description |
|---|---|---|---|
value* | string | — | Identifies the item. Must be unique within the radio group. |
closeOnSelect | boolean | true | Whether selecting the item closes the menu. |
disabled | Signal<boolean> | boolean | false | Prevents selecting the item; the keyboard skips it. |
| Data attribute | Value |
|---|---|
[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.
| Prop | Type | Default | Description |
|---|---|---|---|
open | Signal<boolean> | boolean | false | The 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.
| Prop | Type | Default | Description |
|---|---|---|---|
openDelay | number | 100 | How long the pointer must rest on the trigger before the submenu opens, in milliseconds. |
disabled | Signal<boolean> | boolean | false | Prevents opening the submenu; the keyboard skips the item. |
| Data attribute | Value |
|---|---|
[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.
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
offset | number | 8 | Distance in pixels between the sub trigger and the panel. |
loop | boolean | true | Whether arrow keys wrap from the last item back to the first. |
| Data attribute | Value |
|---|---|
[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 attribute | Value |
|---|---|
[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 attribute | Value |
|---|---|
[data-context-menu-group-heading] | Present |
ContextMenuSeparator
A role="separator" line between sections. Renders a Div; extra props are forwarded onto it.
| Data attribute | Value |
|---|---|
[data-context-menu-separator] | Present |
ContextMenuPortal
The core Portal helper, for rendering the panel into another DOM parent to escape overflow and stacking.