Context Menu
A context menu opens where you right-click, or long-press on a touch screen. It offers actions that apply to the thing under the pointer.
Right-click here, or long-press on a touch screen
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
export default function ContextMenuDemo() {
return (
<ContextMenu>
<ContextMenuTrigger className="flex h-40 w-full max-w-sm items-center justify-center rounded-lg border border-dashed border-border-strong px-4 text-center text-sm text-text-secondary select-none">
Right-click here, or long-press on a touch screen
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>
Back
<ContextMenuShortcut>⌘[</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem disabled>
Forward
<ContextMenuShortcut>⌘]</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
Reload
<ContextMenuShortcut>⌘R</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuCheckboxItem defaultChecked>Show bookmarks</ContextMenuCheckboxItem>
<ContextMenuSeparator />
<ContextMenuItem variant="destructive">Delete</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}The code
The component is a single file, read from the repository when the site is built. Press Show code to see all of it.
import type { ComponentProps } from "react";
import { Check } from "lucide-react";
import { ContextMenu as BaseContextMenu } from "@base-ui/react/context-menu";
import { cx } from "@/lib/cx";
import { menuItemStyles, menuItemTones, popupMotion, popupSurface } from "@/lib/styles";
import type { WithClass } from "@/lib/types";
export const ContextMenu = BaseContextMenu.Root;
// The area that opens the menu when it is right-clicked, or long-pressed on a touch screen. Give
// it a size and a look with classes.
export const ContextMenuTrigger = BaseContextMenu.Trigger;
// The menu opens at the pointer, so there is no side or align to set.
export function ContextMenuContent({
className,
...props
}: WithClass<ComponentProps<typeof BaseContextMenu.Popup>>) {
return (
<BaseContextMenu.Portal>
<BaseContextMenu.Positioner className="z-50">
<BaseContextMenu.Popup className={cx(popupSurface, popupMotion, "min-w-48 p-1", className)} {...props} />
</BaseContextMenu.Positioner>
</BaseContextMenu.Portal>
);
}
// variant="destructive" turns the row red, for something like Log out or Delete.
export function ContextMenuItem({
variant = "default",
className,
...props
}: WithClass<ComponentProps<typeof BaseContextMenu.Item>> & { variant?: keyof typeof menuItemTones }) {
return <BaseContextMenu.Item className={cx(menuItemStyles, menuItemTones[variant], className)} {...props} />;
}
export function ContextMenuSeparator({
className,
...props
}: WithClass<ComponentProps<typeof BaseContextMenu.Separator>>) {
return <BaseContextMenu.Separator className={cx("-mx-1 my-1 h-px bg-border", className)} {...props} />;
}
export const ContextMenuGroup = BaseContextMenu.Group;
export function ContextMenuLabel({
className,
...props
}: WithClass<ComponentProps<typeof BaseContextMenu.GroupLabel>>) {
return (
<BaseContextMenu.GroupLabel className={cx("px-2 py-1.5 text-sm font-medium text-text-secondary", className)} {...props} />
);
}
export function ContextMenuCheckboxItem({
className,
children,
...props
}: WithClass<ComponentProps<typeof BaseContextMenu.CheckboxItem>>) {
return (
<BaseContextMenu.CheckboxItem className={cx(menuItemStyles, menuItemTones.default, "relative pl-8", className)} {...props}>
<span className="absolute left-2 flex size-4 items-center justify-center">
<BaseContextMenu.CheckboxItemIndicator>
<Check aria-hidden className="size-4" />
</BaseContextMenu.CheckboxItemIndicator>
</span>
{children}
</BaseContextMenu.CheckboxItem>
);
}
export const ContextMenuRadioGroup = BaseContextMenu.RadioGroup;
export function ContextMenuRadioItem({
className,
children,
...props
}: WithClass<ComponentProps<typeof BaseContextMenu.RadioItem>>) {
return (
<BaseContextMenu.RadioItem className={cx(menuItemStyles, menuItemTones.default, "relative pl-8", className)} {...props}>
<span className="absolute left-2 flex size-4 items-center justify-center">
<BaseContextMenu.RadioItemIndicator>
<span className="block size-2 rounded-full bg-text" />
</BaseContextMenu.RadioItemIndicator>
</span>
{children}
</BaseContextMenu.RadioItem>
);
}
export function ContextMenuShortcut({ className, ...props }: ComponentProps<"span">) {
return <span className={cx("ml-auto pl-6 text-sm text-text-secondary", className)} {...props} />;
}Props
| Prop | Type | Default | What it does |
|---|---|---|---|
variant | "default" | "destructive" | "default" | On ContextMenuItem. The destructive variant turns the row red, for something like Delete. |
onClick | function | — | On ContextMenuItem. Runs when the item is chosen. The menu closes afterwards. |
disabled | boolean | false | On any item. Dims it and stops it from being chosen. The arrow keys can still reach it. |
defaultChecked | boolean | — | On ContextMenuCheckboxItem. Whether it starts ticked. Use checked and onCheckedChange to control it. |
defaultValue | string | — | On ContextMenuRadioGroup. The value of the radio item that starts chosen. Each ContextMenuRadioItem needs a value. |
Every other prop goes to the Base UI ContextMenu underneath. The menu opens at the pointer, so it has no side or align.
The thinking
A context menu is a shortcut for people who already know it is there. It should never be the only way to do something.
- Built on Base UI: Base UI opens the menu at the pointer, handles the long-press on touch screens, and gives it the same keyboard navigation as any menu.
- Same rows as the Dropdown Menu: The items, tick marks, radio dots, separators and shortcuts are the same, so the two menus look like one family.
- The trigger is the area: ContextMenuTrigger is a box you size and style yourself. Right-clicking anywhere inside it opens the menu.
Interface design
- Surface: The shared overlay surface, the same as the Dropdown Menu, so it appears at the pointer as the same object.
- Rows: A light wash follows the highlighted row. Destructive rows are red, and disabled rows are muted.
- Show it can be done: The demo area has a dashed border, so there is a hint that the box does something. Your own areas should hint too.
Interaction
- Open: Right-click, or long-press on a touch screen. The menu opens at the pointer.
- Keyboard: Once it is open, the arrow keys move through the rows and Enter or Space chooses one. Typing a letter jumps to a row that starts with it.
- Close: Escape, a click outside, or choosing an item.
- Page menu: The browser's own menu is replaced inside the area only.
Empathy
- Never the only way: Many people never right-click, and keyboard and touch users can't easily. Every action here should also be reachable with a visible button or a Dropdown Menu.
- Announced as a menu: Rows are announced with their role, and checkbox rows report whether they are on.
- Tell people it is there: Nothing says a box has a context menu until someone tries. A line of hint text, as in the demo, helps.