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

PropTypeDefaultWhat it does
variant"default" | "destructive""default"On ContextMenuItem. The destructive variant turns the row red, for something like Delete.
onClickfunction—On ContextMenuItem. Runs when the item is chosen. The menu closes afterwards.
disabledbooleanfalseOn any item. Dims it and stops it from being chosen. The arrow keys can still reach it.
defaultCheckedboolean—On ContextMenuCheckboxItem. Whether it starts ticked. Use checked and onCheckedChange to control it.
defaultValuestring—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.