Context Menu
A contextual action menu opened from a pointer or the keyboard.
Preview
Example code
Quarterly roadmapRight-click or press Shift+F10
Playground
modalBlocks interaction outside the menu while it is open.
Usage
tsx
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "earthling-ui/context-menu";
export function Example() {
return (
<>
<ContextMenu>
<ContextMenuTrigger>Quarterly roadmap</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuLabel>Roadmap</ContextMenuLabel>
<ContextMenuItem>Open</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger>Move to</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem>Archive</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
</ContextMenuContent>
</ContextMenu>
</>
);
}
- Ensure the trigger can receive focus so keyboard users can open it with Shift+F10.
- Keep commands specific to the selected object and group related actions with separators.
- Shortcuts shown in the menu are hints; register the actual keyboard commands separately.
Installation
Start with the project setup for React and Tailwind CSS 4. Both paths use the same component and theme.
Import package
Own the source
bash
npm install earthling-uiUse the component import shown above. Package updates keep the implementation current.
API reference
Declarations from the current library build, including inherited primitive props. Playground controls above show selected options; they are not the complete API.
tsx
// Generated by dts-bundle-generator v9.5.1
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
import { ComponentProps } from 'react';
export declare function ContextMenu({ ...props }: ComponentProps<typeof ContextMenuPrimitive.Root>): import("react").JSX.Element;
export declare function ContextMenuTrigger({ ...props }: ComponentProps<typeof ContextMenuPrimitive.Trigger>): import("react").JSX.Element;
export declare function ContextMenuGroup({ ...props }: ComponentProps<typeof ContextMenuPrimitive.Group>): import("react").JSX.Element;
export declare function ContextMenuPortal({ ...props }: ComponentProps<typeof ContextMenuPrimitive.Portal>): import("react").JSX.Element;
export declare function ContextMenuSub({ ...props }: ComponentProps<typeof ContextMenuPrimitive.Sub>): import("react").JSX.Element;
export declare function ContextMenuRadioGroup({ ...props }: ComponentProps<typeof ContextMenuPrimitive.RadioGroup>): import("react").JSX.Element;
export declare function ContextMenuSubTrigger({ className, inset, children, ...props }: ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}): import("react").JSX.Element;
export declare function ContextMenuSubContent({ className, ...props }: ComponentProps<typeof ContextMenuPrimitive.SubContent>): import("react").JSX.Element;
export declare function ContextMenuContent({ className, ...props }: ComponentProps<typeof ContextMenuPrimitive.Content>): import("react").JSX.Element;
export declare function ContextMenuItem({ className, inset, ...props }: ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
}): import("react").JSX.Element;
export declare function ContextMenuCheckboxItem({ className, children, checked, ...props }: ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>): import("react").JSX.Element;
export declare function ContextMenuRadioItem({ className, children, ...props }: ComponentProps<typeof ContextMenuPrimitive.RadioItem>): import("react").JSX.Element;
export declare function ContextMenuLabel({ className, inset, ...props }: ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}): import("react").JSX.Element;
export declare function ContextMenuSeparator({ className, ...props }: ComponentProps<typeof ContextMenuPrimitive.Separator>): import("react").JSX.Element;
export declare function ContextMenuShortcut({ className, ...props }: ComponentProps<"span">): import("react").JSX.Element;
export {};Runtime dependencies: @radix-ui/react-context-menu, cnfast.
Source
Use the eject command to copy this implementation with its required helpers and project-specific imports.
View Context Menu implementation
tsx
"use client";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { cn } from "@/utils/cn";
import type { ComponentProps } from "react";
function ContextMenu({
...props
}: ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
}
function ContextMenuTrigger({
...props
}: ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
);
}
function ContextMenuGroup({
...props
}: ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
);
}
function ContextMenuPortal({
...props
}: ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
);
}
function ContextMenuSub({
...props
}: ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
}
function ContextMenuRadioGroup({
...props
}: ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
);
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-muted focus:text-foreground data-[state=open]:bg-muted data-[state=open]:text-foreground flex cursor-default items-center rounded-lg px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:ps-8",
className,
)}
{...props}
>
{children}
<i className="ms-auto size-4 icon-[lucide--chevron-right] rtl:rotate-180" />
</ContextMenuPrimitive.SubTrigger>
);
}
function ContextMenuSubContent({
className,
...props
}: ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"bg-background text-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border p-1 shadow-lg duration-150 ease-out motion-reduce:animate-none",
className,
)}
{...props}
/>
);
}
function ContextMenuContent({
className,
...props
}: ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"bg-background text-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border p-1 shadow-md duration-150 ease-out motion-reduce:animate-none",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
);
}
function ContextMenuItem({
className,
inset,
...props
}: ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
className={cn(
"focus:bg-muted focus:text-foreground relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
}: ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"focus:bg-muted focus:text-foreground relative flex cursor-default items-center gap-2 rounded-lg py-1.5 pe-2 ps-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute start-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<i className="size-4 icon-[lucide--check]" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
);
}
function ContextMenuRadioItem({
className,
children,
...props
}: ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"focus:bg-muted focus:text-foreground relative flex cursor-default items-center gap-2 rounded-lg py-1.5 pe-2 ps-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="pointer-events-none absolute start-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<i className="size-2 fill-current icon-[lucide--circle]" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
);
}
function ContextMenuLabel({
className,
inset,
...props
}: ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground data-[inset]:ps-8",
className,
)}
{...props}
/>
);
}
function ContextMenuSeparator({
className,
...props
}: ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
);
}
function ContextMenuShortcut({ className, ...props }: ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"ms-auto text-xs tracking-widest text-muted-foreground",
className,
)}
{...props}
/>
);
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};