Context Menu
A menu that opens where you right-click or long press.
Right-click this screenshot
Installation
npx shadcn@latest add @runqora/craft-context-menuUsage
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/craft/context-menu";
<ContextMenu>
<ContextMenuTrigger tabIndex={0}>Right-click here</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Open</ContextMenuItem>
<ContextMenuItem variant="destructive">Delete</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>;Props
ContextMenu
Accessibility
- Right-click, or a long press on a touch screen, opens the menu at the pointer.
- From the keyboard, the context menu key or Shift+F10 opens it while focus is inside the trigger. Give the trigger
tabIndex={0}if nothing in it can take focus. - Inside, it works like a Dropdown Menu: the arrow keys move, Right Arrow opens a submenu and Escape closes.
- Anything in a context menu should also be reachable another way, as many people never right-click.
Customization
The items are Dropdown Menu's, renamed, so a context menu has the same groups, checkbox and radio items, submenus and destructive actions.
Dependencies
@base-ui/reactcraft-dropdown-menu- shadcn's
utils, andcraft-theme
Source
"use client";
import { ContextMenu as BaseContextMenu } from "@base-ui/react/context-menu";
import type { ComponentProps } from "react";
import {
DropdownMenuCheckboxItem,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuLinkItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
} from "@/components/craft/dropdown-menu";
import { menuPopupClassName } from "@/components/craft/menu-styles";
import { cn } from "@/lib/utils";
export function ContextMenu(
props: ComponentProps<typeof BaseContextMenu.Root>
) {
return <BaseContextMenu.Root {...props} />;
}
/**
* The area that opens the menu on right-click, or on a long press on touch
* screens. Keyboard users open it with the context menu key or Shift+F10 while
* focus is inside it, so give it `tabIndex={0}` if nothing in it can take focus.
*/
export function ContextMenuTrigger({
className,
...props
}: ComponentProps<typeof BaseContextMenu.Trigger>) {
return (
<BaseContextMenu.Trigger
className={cn("select-none", className)}
data-slot="context-menu-trigger"
{...props}
/>
);
}
export function ContextMenuContent({
className,
...props
}: ComponentProps<typeof BaseContextMenu.Popup>) {
return (
<BaseContextMenu.Portal>
<BaseContextMenu.Positioner className="z-50 outline-none">
<BaseContextMenu.Popup
className={cn(menuPopupClassName, className)}
data-slot="context-menu-content"
{...props}
/>
</BaseContextMenu.Positioner>
</BaseContextMenu.Portal>
);
}
// A context menu's items are the same as a dropdown menu's, so they're shared.
export const ContextMenuCheckboxItem = DropdownMenuCheckboxItem;
export const ContextMenuGroup = DropdownMenuGroup;
export const ContextMenuItem = DropdownMenuItem;
export const ContextMenuLabel = DropdownMenuLabel;
export const ContextMenuLinkItem = DropdownMenuLinkItem;
export const ContextMenuRadioGroup = DropdownMenuRadioGroup;
export const ContextMenuRadioItem = DropdownMenuRadioItem;
export const ContextMenuSeparator = DropdownMenuSeparator;
export const ContextMenuShortcut = DropdownMenuShortcut;
export const ContextMenuSub = DropdownMenuSub;
export const ContextMenuSubContent = DropdownMenuSubContent;
export const ContextMenuSubTrigger = DropdownMenuSubTrigger;// The look shared by every list that opens over the page: Select, Combobox,
// Dropdown Menu, Context Menu and Navigation Menu. As in Ore UI, it's a dark
// outline around a line, with rows split by that same line. Class names only,
// so it has no "use client".
/** The frame. Its background shows between the rows as the dividing lines. */
export const menuFrameClassName =
"border-craft-outline bg-craft-menu-line text-craft-text border-(length:--craft-unit) p-(--craft-unit) outline-none data-ending-style:opacity-0 data-starting-style:opacity-0 motion-safe:transition-opacity motion-safe:duration-100";
/** Stacks rows with one unit between them, where the frame shows through. */
export const menuListClassName = "flex flex-col gap-(--craft-unit)";
/** The whole popup of a menu, which scrolls when it doesn't fit. */
export const menuPopupClassName = `${menuFrameClassName} ${menuListClassName} max-h-(--available-height) min-w-48 overflow-y-auto`;
export const menuItemClassName =
"bg-craft-menu font-craft-display text-craft-text flex min-h-[calc(var(--craft-unit)*23)] cursor-pointer items-center gap-3 px-(--craft-space) text-left text-(length:--craft-label-size) leading-tight outline-none select-none data-highlighted:bg-craft-menu-highlight focus-visible:craft-focus focus-visible:-outline-offset-2 data-disabled:text-craft-track data-disabled:cursor-not-allowed [&_svg]:shrink-0";
/** A destructive item: red text, and a red row while it's highlighted. */
export const menuItemDestructiveClassName =
"text-craft-destructive-text data-highlighted:bg-craft-destructive data-highlighted:text-craft-destructive-fg";
export const menuGroupLabelClassName =
"bg-craft-menu font-craft-body text-craft-text-muted px-(--craft-space) pt-2 pb-1 text-sm";
/** A heavier divider than the line between rows. */
export const menuSeparatorClassName =
"bg-craft-outline h-(--craft-unit) shrink-0";