# Context menu

> A menu of actions on right click or long press, with submenus, checkbox and radio items, and hold feedback on touch.

Docs: https://hextaui.com/docs/context-menu
Markdown: https://hextaui.com/docs/context-menu.md

```tsx title="components/examples/context-menu/demo.tsx"
"use client"

import * as React from "react"

import {
  ContextMenu,
  ContextMenuCheckboxItem,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuLabel,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"

export function ContextMenuDemo() {
  const [bookmarks, setBookmarks] = React.useState(true)
  const [urls, setUrls] = React.useState(false)
  const [person, setPerson] = React.useState("pedro")
  const [last, setLast] = React.useState("Nothing yet")

  return (
    <div className="flex w-full max-w-sm flex-col gap-2">
      <ContextMenu>
        <ContextMenuTrigger className="flex h-40 w-full items-center justify-center rounded-xl border border-dashed text-sm text-muted-foreground">
          Right click here
        </ContextMenuTrigger>
        <ContextMenuContent>
          <ContextMenuItem inset onClick={() => setLast("Back")}>
            Back
            <ContextMenuShortcut>⌘[</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuItem inset disabled>
            Forward
            <ContextMenuShortcut>⌘]</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuItem inset onClick={() => setLast("Reload")}>
            Reload
            <ContextMenuShortcut>⌘R</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuSub>
            <ContextMenuSubTrigger inset>More tools</ContextMenuSubTrigger>
            <ContextMenuSubContent>
              <ContextMenuItem onClick={() => setLast("Save page")}>
                Save page as…
                <ContextMenuShortcut>⇧⌘S</ContextMenuShortcut>
              </ContextMenuItem>
              <ContextMenuItem onClick={() => setLast("Create shortcut")}>
                Create shortcut…
              </ContextMenuItem>
              <ContextMenuItem onClick={() => setLast("Name window")}>
                Name window…
              </ContextMenuItem>
              <ContextMenuSeparator />
              <ContextMenuItem onClick={() => setLast("Developer tools")}>
                Developer tools
              </ContextMenuItem>
            </ContextMenuSubContent>
          </ContextMenuSub>
          <ContextMenuSeparator />
          <ContextMenuCheckboxItem
            checked={bookmarks}
            onCheckedChange={setBookmarks}
          >
            Show bookmarks bar
            <ContextMenuShortcut>⇧⌘B</ContextMenuShortcut>
          </ContextMenuCheckboxItem>
          <ContextMenuCheckboxItem checked={urls} onCheckedChange={setUrls}>
            Show full URLs
          </ContextMenuCheckboxItem>
          <ContextMenuSeparator />
          <ContextMenuRadioGroup value={person} onValueChange={setPerson}>
            <ContextMenuLabel inset>People</ContextMenuLabel>
            <ContextMenuRadioItem value="pedro">
              Pedro Duarte
            </ContextMenuRadioItem>
            <ContextMenuRadioItem value="colm">Colm Tuite</ContextMenuRadioItem>
          </ContextMenuRadioGroup>
        </ContextMenuContent>
      </ContextMenu>
      <p className="text-sm text-muted-foreground">Last action: {last}</p>
    </div>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/context-menu.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react @tabler/icons-react class-variance-authority cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/context-menu.tsx"
"use client"

import * as React from "react"
import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"
import {
  DirectionProvider,
  useDirection,
  type TextDirection,
} from "@base-ui/react/direction-provider"
import {
  IconCheck,
  IconChevronRight,
  IconCircleFilled,
} from "@tabler/icons-react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

type ContextMenuContextValue = {
  disabled: boolean
  direction: TextDirection
  chosen: string | null
  choose: (id: string) => void
  readDirection: (node: Element) => void
}

const ContextMenuContext = React.createContext<ContextMenuContextValue | null>(
  null
)

const ContextMenuGroupContext = React.createContext(false)

function useContextMenuContext(part: string) {
  const context = React.useContext(ContextMenuContext)
  if (!context) {
    throw new Error(`${part} must be used within <ContextMenu>.`)
  }
  return context
}

const holdDelay = 120
const holdMoveThreshold = 10

function ContextMenu({
  disabled = false,
  onOpenChange,
  onOpenChangeComplete,
  children,
  ...props
}: ContextMenuPrimitive.Root.Props) {
  const inherited = useDirection()
  const [domDirection, setDomDirection] = React.useState<TextDirection>("ltr")
  const [chosen, setChosen] = React.useState<string | null>(null)
  const direction = domDirection === "rtl" ? "rtl" : inherited

  const readDirection = React.useCallback((node: Element) => {
    if (node.isConnected) {
      setDomDirection(
        getComputedStyle(node).direction === "rtl" ? "rtl" : "ltr"
      )
    }
  }, [])

  const context = React.useMemo(
    () => ({
      disabled,
      direction,
      chosen,
      choose: setChosen,
      readDirection,
    }),
    [disabled, direction, chosen, readDirection]
  )

  return (
    <ContextMenuContext.Provider value={context}>
      <DirectionProvider direction={direction}>
        <ContextMenuPrimitive.Root
          disabled={disabled}
          onOpenChange={(open, eventDetails) => {
            onOpenChange?.(open, eventDetails)
            if (open && !eventDetails.isCanceled) {
              setChosen(null)
            }
          }}
          onOpenChangeComplete={(open) => {
            onOpenChangeComplete?.(open)
            if (!open) {
              setChosen(null)
            }
          }}
          {...props}
        >
          {children}
        </ContextMenuPrimitive.Root>
      </DirectionProvider>
    </ContextMenuContext.Provider>
  )
}

type ContextMenuTriggerProps = ContextMenuPrimitive.Trigger.Props & {
  holdFeedback?: boolean
}

function ContextMenuTrigger({
  className,
  holdFeedback = true,
  onContextMenu,
  onTouchStart,
  onTouchMove,
  onTouchEnd,
  onTouchCancel,
  ref,
  ...props
}: ContextMenuTriggerProps) {
  const { disabled, readDirection } =
    useContextMenuContext("ContextMenuTrigger")
  const [holding, setHolding] = React.useState(false)
  const timerRef = React.useRef<ReturnType<typeof setTimeout> | undefined>(
    undefined
  )
  const originRef = React.useRef<{ x: number; y: number } | null>(null)

  const release = React.useCallback(() => {
    clearTimeout(timerRef.current)
    originRef.current = null
    setHolding(false)
  }, [])

  React.useEffect(() => () => clearTimeout(timerRef.current), [])

  const setRef = React.useCallback(
    (node: HTMLDivElement | null) => {
      if (node) {
        readDirection(node)
      }
      if (typeof ref === "function") {
        return ref(node)
      }
      if (ref) {
        ref.current = node
      }
    },
    [ref, readDirection]
  )

  return (
    <ContextMenuPrimitive.Trigger
      ref={setRef}
      data-slot="context-menu-trigger"
      data-holding={holding ? "" : undefined}
      className={mergeClassName(
        "transition-[scale,box-shadow] duration-300 ease-spring outline-none select-none focus-visible:outline-hidden data-popup-open:inset-ring-3 data-popup-open:inset-ring-ring/50 motion-safe:data-holding:not-data-popup-open:scale-98 motion-safe:data-holding:not-data-popup-open:duration-[380ms] motion-safe:data-holding:not-data-popup-open:ease-out-cubic motion-reduce:transition-shadow",
        className
      )}
      onContextMenu={(event) => {
        onContextMenu?.(event)
        readDirection(event.currentTarget)
      }}
      onTouchStart={(event) => {
        onTouchStart?.(event)
        release()
        if (
          !holdFeedback ||
          disabled ||
          event.defaultPrevented ||
          event.touches.length !== 1
        ) {
          return
        }
        readDirection(event.currentTarget)
        const touch = event.touches[0]
        originRef.current = { x: touch.clientX, y: touch.clientY }
        timerRef.current = setTimeout(() => setHolding(true), holdDelay)
      }}
      onTouchMove={(event) => {
        onTouchMove?.(event)
        const origin = originRef.current
        const touch = event.touches[0]
        if (
          origin &&
          (event.touches.length !== 1 ||
            Math.abs(touch.clientX - origin.x) > holdMoveThreshold ||
            Math.abs(touch.clientY - origin.y) > holdMoveThreshold)
        ) {
          release()
        }
      }}
      onTouchEnd={(event) => {
        onTouchEnd?.(event)
        release()
      }}
      onTouchCancel={(event) => {
        onTouchCancel?.(event)
        release()
      }}
      {...props}
    />
  )
}

function ContextMenuPortal(props: ContextMenuPrimitive.Portal.Props) {
  return (
    <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
  )
}

type ContextMenuContentProps = ContextMenuPrimitive.Popup.Props &
  Pick<
    ContextMenuPrimitive.Positioner.Props,
    | "side"
    | "align"
    | "sideOffset"
    | "alignOffset"
    | "anchor"
    | "collisionPadding"
    | "collisionAvoidance"
  >

const contextMenuContentClassName =
  "relative max-h-(--available-height) max-w-[min(var(--available-width),20rem)] min-w-40 origin-(--transform-origin) overflow-x-hidden overflow-y-auto overscroll-none rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-(length:--hairline) forced-colors:border ring-foreground/10 transition-[opacity,scale] duration-150 ease-out-quint outline-none focus-visible:outline-hidden [--context-menu-item-radius:max(calc(var(--radius-sm)*0.5),calc(var(--radius-lg)-0.25rem))] data-ending-style:opacity-0 data-starting-style:opacity-0 data-ending-style:data-instant:transition-none motion-safe:data-starting-style:scale-96 motion-safe:data-[chosen]:data-ending-style:delay-120 motion-reduce:transition-opacity"

function useContentPositioning(part: string) {
  const { direction, chosen } = useContextMenuContext(part)
  return {
    dir: direction === "rtl" ? "rtl" : undefined,
    chosen: chosen === null ? undefined : "",
  }
}

function ContextMenuContent({
  className,
  side,
  align,
  sideOffset,
  alignOffset,
  anchor,
  collisionPadding,
  collisionAvoidance,
  dir,
  ...props
}: ContextMenuContentProps) {
  const positioning = useContentPositioning("ContextMenuContent")

  return (
    <ContextMenuPrimitive.Portal>
      <ContextMenuPrimitive.Positioner
        data-slot="context-menu-positioner"
        dir={positioning.dir}
        side={side}
        align={align}
        sideOffset={sideOffset}
        alignOffset={alignOffset}
        anchor={anchor}
        collisionPadding={collisionPadding}
        collisionAvoidance={collisionAvoidance}
        className="isolate z-50 outline-none focus-visible:outline-hidden"
      >
        <ContextMenuPrimitive.Popup
          data-slot="context-menu-content"
          data-chosen={positioning.chosen}
          dir={dir}
          className={mergeClassName(contextMenuContentClassName, className)}
          {...props}
        />
      </ContextMenuPrimitive.Positioner>
    </ContextMenuPrimitive.Portal>
  )
}

function ContextMenuGroup(props: ContextMenuPrimitive.Group.Props) {
  return (
    <ContextMenuGroupContext.Provider value>
      <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
    </ContextMenuGroupContext.Provider>
  )
}

type ContextMenuLabelProps = ContextMenuPrimitive.GroupLabel.Props & {
  inset?: boolean
}

const contextMenuLabelClassName =
  "px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground wrap-anywhere data-inset:ps-8"

function ContextMenuStandaloneLabel({
  className,
  render,
  ...props
}: ContextMenuPrimitive.GroupLabel.Props) {
  const resolved = mergeClassName(contextMenuLabelClassName, className)

  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: typeof resolved === "function" ? resolved({}) : resolved,
      },
      props as React.ComponentProps<"div">
    ),
  })
}

function ContextMenuLabel({ inset, ...props }: ContextMenuLabelProps) {
  const inGroup = React.useContext(ContextMenuGroupContext)
  const Label = inGroup
    ? ContextMenuPrimitive.GroupLabel
    : ContextMenuStandaloneLabel

  return (
    <Label
      data-slot="context-menu-label"
      data-inset={inset ? "" : undefined}
      {...props}
      className={
        inGroup
          ? mergeClassName(contextMenuLabelClassName, props.className)
          : props.className
      }
    />
  )
}

const contextMenuItemVariants = cva(
  "relative flex min-h-8 w-full min-w-0 cursor-default items-center gap-2 rounded-(--context-menu-item-radius) px-2 py-1.5 text-start text-sm wrap-anywhere outline-none select-none focus-visible:outline-hidden data-highlighted:bg-accent data-highlighted:text-accent-foreground data-inset:ps-8 motion-safe:data-[chosen]:animate-menu-blink forced-colors:data-highlighted:outline-2 forced-colors:data-highlighted:-outline-offset-2 forced-colors:data-highlighted:outline-solid pointer-coarse:min-h-11 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-highlighted:[&_svg:not([class*='text-'])]:text-accent-foreground",
  {
    variants: {
      variant: {
        default: "",
        destructive:
          "text-destructive [--menu-blink:color-mix(in_oklab,var(--color-destructive)_10%,transparent)] data-highlighted:bg-destructive/10 data-highlighted:text-destructive dark:[--menu-blink:color-mix(in_oklab,var(--color-destructive)_20%,transparent)] dark:data-highlighted:bg-destructive/20 forced-colors:data-highlighted:outline-2 forced-colors:data-highlighted:-outline-offset-2 forced-colors:data-highlighted:outline-solid [&_svg:not([class*='text-'])]:text-destructive data-highlighted:[&_svg:not([class*='text-'])]:text-destructive",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

type ChoosableEvent = React.MouseEvent<HTMLElement> & {
  baseUIHandlerPrevented?: boolean
}

function useChooseOnClick<E extends ChoosableEvent>(
  closeOnClick: boolean,
  onClick: ((event: E) => void) | undefined
) {
  const context = React.useContext(ContextMenuContext)
  const id = React.useId()

  return {
    chosen: context?.chosen === id ? "" : undefined,
    onClick: (event: E) => {
      onClick?.(event)
      if (
        context &&
        closeOnClick &&
        event.detail !== 0 &&
        !event.defaultPrevented &&
        !event.baseUIHandlerPrevented
      ) {
        context.choose(id)
      }
    },
  }
}

type ContextMenuItemProps = ContextMenuPrimitive.Item.Props &
  VariantProps<typeof contextMenuItemVariants> & {
    inset?: boolean
  }

function ContextMenuItem({
  className,
  variant = "default",
  inset,
  closeOnClick = true,
  onClick,
  ...props
}: ContextMenuItemProps) {
  const choice = useChooseOnClick(closeOnClick, onClick)

  return (
    <ContextMenuPrimitive.Item
      data-slot="context-menu-item"
      data-variant={variant}
      data-inset={inset ? "" : undefined}
      data-chosen={choice.chosen}
      closeOnClick={closeOnClick}
      onClick={choice.onClick}
      className={mergeClassName(
        contextMenuItemVariants({ variant }),
        className
      )}
      {...props}
    />
  )
}

const contextMenuIndicatorClassName =
  "flex items-center justify-center opacity-0 transition-[opacity,scale] duration-150 ease-out-quint data-checked:opacity-100 motion-safe:scale-50 motion-safe:data-checked:scale-100 motion-reduce:transition-none"

type ContextMenuCheckboxItemProps = ContextMenuPrimitive.CheckboxItem.Props & {
  inset?: boolean
}

function ContextMenuCheckboxItem({
  className,
  children,
  inset,
  closeOnClick = false,
  onClick,
  ...props
}: ContextMenuCheckboxItemProps) {
  const choice = useChooseOnClick(closeOnClick, onClick)

  return (
    <ContextMenuPrimitive.CheckboxItem
      data-slot="context-menu-checkbox-item"
      data-inset={inset ? "" : undefined}
      data-chosen={choice.chosen}
      closeOnClick={closeOnClick}
      onClick={choice.onClick}
      className={mergeClassName(
        cn(contextMenuItemVariants(), "ps-8"),
        className
      )}
      {...props}
    >
      <span
        aria-hidden
        className="pointer-events-none absolute start-2 flex size-4 items-center justify-center"
      >
        <ContextMenuPrimitive.CheckboxItemIndicator
          keepMounted
          data-slot="context-menu-checkbox-item-indicator"
          className={contextMenuIndicatorClassName}
        >
          <IconCheck className="text-current" />
        </ContextMenuPrimitive.CheckboxItemIndicator>
      </span>
      {children}
    </ContextMenuPrimitive.CheckboxItem>
  )
}

function ContextMenuRadioGroup(props: ContextMenuPrimitive.RadioGroup.Props) {
  return (
    <ContextMenuGroupContext.Provider value>
      <ContextMenuPrimitive.RadioGroup
        data-slot="context-menu-radio-group"
        {...props}
      />
    </ContextMenuGroupContext.Provider>
  )
}

type ContextMenuRadioItemProps = ContextMenuPrimitive.RadioItem.Props & {
  inset?: boolean
}

function ContextMenuRadioItem({
  className,
  children,
  inset,
  closeOnClick = false,
  onClick,
  ...props
}: ContextMenuRadioItemProps) {
  const choice = useChooseOnClick(closeOnClick, onClick)

  return (
    <ContextMenuPrimitive.RadioItem
      data-slot="context-menu-radio-item"
      data-inset={inset ? "" : undefined}
      data-chosen={choice.chosen}
      closeOnClick={closeOnClick}
      onClick={choice.onClick}
      className={mergeClassName(
        cn(contextMenuItemVariants(), "ps-8"),
        className
      )}
      {...props}
    >
      <span
        aria-hidden
        className="pointer-events-none absolute start-2 flex size-4 items-center justify-center"
      >
        <ContextMenuPrimitive.RadioItemIndicator
          keepMounted
          data-slot="context-menu-radio-item-indicator"
          className={contextMenuIndicatorClassName}
        >
          <IconCircleFilled className="size-2 text-current" />
        </ContextMenuPrimitive.RadioItemIndicator>
      </span>
      {children}
    </ContextMenuPrimitive.RadioItem>
  )
}

function ContextMenuSeparator({
  className,
  ...props
}: ContextMenuPrimitive.Separator.Props) {
  return (
    <ContextMenuPrimitive.Separator
      data-slot="context-menu-separator"
      className={mergeClassName("-mx-1 my-1 h-px bg-border", className)}
      {...props}
    />
  )
}

function ContextMenuShortcut({
  className,
  dir = "ltr",
  children,
  ...props
}: React.ComponentProps<"kbd">) {
  return (
    <kbd
      data-slot="context-menu-shortcut"
      className={cn(
        "ms-auto shrink-0 ps-3 font-sans text-xs tracking-widest text-muted-foreground in-data-highlighted:text-accent-foreground/70 in-data-[variant=destructive]:text-destructive/70",
        className
      )}
      {...props}
    >
      <span dir={dir} className="[unicode-bidi:isolate]">
        {children}
      </span>
    </kbd>
  )
}

function ContextMenuSub(props: ContextMenuPrimitive.SubmenuRoot.Props) {
  return <ContextMenuPrimitive.SubmenuRoot {...props} />
}

type ContextMenuSubTriggerProps = ContextMenuPrimitive.SubmenuTrigger.Props & {
  inset?: boolean
}

function ContextMenuSubTrigger({
  className,
  children,
  inset,
  ...props
}: ContextMenuSubTriggerProps) {
  return (
    <ContextMenuPrimitive.SubmenuTrigger
      data-slot="context-menu-sub-trigger"
      data-inset={inset ? "" : undefined}
      className={mergeClassName(
        cn(
          contextMenuItemVariants(),
          "data-popup-open:bg-accent data-popup-open:text-accent-foreground forced-colors:data-popup-open:outline-2 forced-colors:data-popup-open:-outline-offset-2 forced-colors:data-popup-open:outline-solid"
        ),
        className
      )}
      {...props}
    >
      {children}
      <IconChevronRight
        aria-hidden
        data-slot="context-menu-sub-trigger-icon"
        className="ms-auto rtl:-scale-x-100"
      />
    </ContextMenuPrimitive.SubmenuTrigger>
  )
}

function ContextMenuSubContent({
  className,
  side,
  align,
  sideOffset = 0,
  alignOffset = -4,
  anchor,
  collisionPadding,
  collisionAvoidance,
  dir,
  ...props
}: ContextMenuContentProps) {
  const positioning = useContentPositioning("ContextMenuSubContent")

  return (
    <ContextMenuPrimitive.Portal>
      <ContextMenuPrimitive.Positioner
        data-slot="context-menu-sub-positioner"
        dir={positioning.dir}
        side={side}
        align={align}
        sideOffset={sideOffset}
        alignOffset={alignOffset}
        anchor={anchor}
        collisionPadding={collisionPadding}
        collisionAvoidance={collisionAvoidance}
        className="isolate z-50 outline-none focus-visible:outline-hidden"
      >
        <ContextMenuPrimitive.Popup
          data-slot="context-menu-sub-content"
          data-chosen={positioning.chosen}
          dir={dir}
          className={mergeClassName(contextMenuContentClassName, className)}
          {...props}
        />
      </ContextMenuPrimitive.Positioner>
    </ContextMenuPrimitive.Portal>
  )
}

export {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuPortal,
  ContextMenuContent,
  ContextMenuGroup,
  ContextMenuLabel,
  ContextMenuItem,
  ContextMenuCheckboxItem,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuSub,
  ContextMenuSubTrigger,
  ContextMenuSubContent,
  contextMenuItemVariants,
}
export type {
  ContextMenuTriggerProps,
  ContextMenuContentProps,
  ContextMenuLabelProps,
  ContextMenuItemProps,
  ContextMenuCheckboxItemProps,
  ContextMenuRadioItemProps,
  ContextMenuSubTriggerProps,
}
```

Update the import paths to match your project setup.

## Usage

```tsx
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
```

```tsx
<ContextMenu>
  <ContextMenuTrigger>Right click here</ContextMenuTrigger>
  <ContextMenuContent>
    <ContextMenuItem>Profile</ContextMenuItem>
    <ContextMenuItem>Billing</ContextMenuItem>
    <ContextMenuItem variant="destructive">Delete</ContextMenuItem>
  </ContextMenuContent>
</ContextMenu>
```

## Composition

```text
ContextMenu
├── ContextMenuTrigger
└── ContextMenuContent
    ├── ContextMenuGroup
    │   ├── ContextMenuLabel
    │   └── ContextMenuItem
    │       └── ContextMenuShortcut
    ├── ContextMenuCheckboxItem
    ├── ContextMenuRadioGroup
    │   └── ContextMenuRadioItem
    ├── ContextMenuSeparator
    └── ContextMenuSub
        ├── ContextMenuSubTrigger
        └── ContextMenuSubContent
```

## Examples

### File list

Give every row its own menu. The open row keeps a highlight, and a destructive item hands off to an alert dialog for confirmation.

```tsx title="components/examples/context-menu/file-list.tsx"
"use client"

import * as React from "react"
import {
  IconCopy,
  IconDownload,
  IconFile,
  IconFolder,
  IconLink,
  IconMail,
  IconPencil,
  IconRefresh,
  IconShare,
  IconStar,
  IconTrash,
} from "@tabler/icons-react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import {
  ContextMenu,
  ContextMenuCheckboxItem,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"

const files = [
  { name: "Quarterly report.pdf", kind: "file" },
  { name: "Brand assets", kind: "folder" },
  { name: "Roadmap 2027.md", kind: "file" },
] as const

function FileRow({
  name,
  kind,
  onDelete,
}: {
  name: string
  kind: "file" | "folder"
  onDelete: () => void
}) {
  const [starred, setStarred] = React.useState(false)
  const Icon = kind === "folder" ? IconFolder : IconFile

  return (
    <ContextMenu>
      <ContextMenuTrigger className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm hover:bg-muted/50 data-popup-open:bg-muted">
        <Icon className="size-4 text-muted-foreground" aria-hidden />
        <span className="min-w-0 flex-1 truncate">{name}</span>
        {starred ? (
          <IconStar
            className="size-4 text-muted-foreground"
            aria-label="Starred"
          />
        ) : null}
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem>
          <IconPencil />
          Rename…
        </ContextMenuItem>
        <ContextMenuItem>
          <IconCopy />
          Duplicate
          <ContextMenuShortcut>⌘D</ContextMenuShortcut>
        </ContextMenuItem>
        <ContextMenuItem>
          <IconDownload />
          Download
        </ContextMenuItem>
        <ContextMenuCheckboxItem checked={starred} onCheckedChange={setStarred}>
          Starred
        </ContextMenuCheckboxItem>
        <ContextMenuSub>
          <ContextMenuSubTrigger>
            <IconShare />
            Share
          </ContextMenuSubTrigger>
          <ContextMenuSubContent>
            <ContextMenuItem>
              <IconMail />
              Email
            </ContextMenuItem>
            <ContextMenuItem>
              <IconLink />
              Copy link
            </ContextMenuItem>
          </ContextMenuSubContent>
        </ContextMenuSub>
        <ContextMenuSeparator />
        <ContextMenuItem variant="destructive" onClick={onDelete}>
          <IconTrash />
          Delete
          <ContextMenuShortcut>⌘⌫</ContextMenuShortcut>
        </ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}

export function ContextMenuFileList() {
  const [pending, setPending] = React.useState<string | null>(null)
  const [removed, setRemoved] = React.useState<string[]>([])

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <div className="flex flex-col gap-0.5 rounded-xl border p-1">
        {files
          .filter((file) => !removed.includes(file.name))
          .map((file) => (
            <FileRow
              key={file.name}
              name={file.name}
              kind={file.kind}
              onDelete={() => setPending(file.name)}
            />
          ))}
      </div>
      <div>
        <Button variant="outline" size="sm" onClick={() => setRemoved([])}>
          <IconRefresh />
          Restore files
        </Button>
      </div>
      <AlertDialog
        open={pending !== null}
        onOpenChange={(open) => {
          if (!open) {
            setPending(null)
          }
        }}
      >
        <AlertDialogContent size="sm">
          <AlertDialogHeader>
            <AlertDialogTitle>Delete “{pending}”?</AlertDialogTitle>
            <AlertDialogDescription>
              It moves to the trash for 30 days.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancel</AlertDialogCancel>
            <AlertDialogAction
              variant="destructive"
              onClick={() => {
                if (pending) {
                  setRemoved((value) => [...value, pending])
                }
                setPending(null)
              }}
            >
              Delete
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </div>
  )
}
```

### Controlled

Pass `open` and `onOpenChange` to own the state. The second argument says why it changed, such as `trigger-press`, `outside-press` or `escape-key`. Items with `closeOnClick={false}` keep it open.

```tsx title="components/examples/context-menu/controlled.tsx"
"use client"

import * as React from "react"
import { IconCopy, IconScissors } from "@tabler/icons-react"

import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"

export function ContextMenuControlled() {
  const [open, setOpen] = React.useState(false)
  const [reason, setReason] = React.useState("—")

  return (
    <div className="flex w-full max-w-sm flex-col gap-2">
      <ContextMenu
        open={open}
        onOpenChange={(next, details) => {
          setOpen(next)
          setReason(details.reason)
        }}
      >
        <ContextMenuTrigger className="flex h-40 w-full items-center justify-center rounded-xl border border-dashed text-sm text-muted-foreground">
          Controlled area
        </ContextMenuTrigger>
        <ContextMenuContent>
          <ContextMenuItem>
            <IconScissors />
            Cut
          </ContextMenuItem>
          <ContextMenuItem>
            <IconCopy />
            Copy
          </ContextMenuItem>
          <ContextMenuItem closeOnClick={false}>
            Stays open on click
          </ContextMenuItem>
        </ContextMenuContent>
      </ContextMenu>
      <p className="text-sm text-muted-foreground">
        Open: {String(open)} · last reason: {reason}
      </p>
    </div>
  )
}
```

### Disabled

A disabled `<ContextMenu />` gives the area back to the browser’s own menu. Disabled items stay visible but are skipped by the keyboard.

```tsx title="components/examples/context-menu/disabled.tsx"
import { IconTrash } from "@tabler/icons-react"

import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"

export function ContextMenuDisabled() {
  return (
    <div className="grid w-full max-w-lg gap-3 sm:grid-cols-2">
      <ContextMenu disabled>
        <ContextMenuTrigger className="flex h-40 w-full items-center justify-center rounded-xl border border-dashed text-sm text-muted-foreground">
          Disabled menu
        </ContextMenuTrigger>
        <ContextMenuContent>
          <ContextMenuItem>Never shown</ContextMenuItem>
        </ContextMenuContent>
      </ContextMenu>
      <ContextMenu>
        <ContextMenuTrigger className="flex h-40 w-full items-center justify-center rounded-xl border border-dashed text-sm text-muted-foreground">
          Disabled items
        </ContextMenuTrigger>
        <ContextMenuContent>
          <ContextMenuItem>Enabled</ContextMenuItem>
          <ContextMenuItem disabled>Disabled item</ContextMenuItem>
          <ContextMenuItem variant="destructive" disabled>
            <IconTrash />
            Disabled destructive
          </ContextMenuItem>
        </ContextMenuContent>
      </ContextMenu>
    </div>
  )
}
```

### Hold feedback

On touch screens the menu opens after a long press. While the finger is held, the area shrinks slightly so people know the press registered. Moving the finger cancels it. Set `holdFeedback={false}` to turn it off.

```tsx title="components/examples/context-menu/hold-feedback.tsx"
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"

export function ContextMenuHoldFeedback() {
  return (
    <div className="grid w-full max-w-lg gap-3 sm:grid-cols-2">
      <ContextMenu>
        <ContextMenuTrigger className="flex h-40 w-full items-center justify-center rounded-xl border border-dashed text-sm text-muted-foreground">
          Press and hold
        </ContextMenuTrigger>
        <ContextMenuContent>
          <ContextMenuItem>Copy</ContextMenuItem>
          <ContextMenuItem>Paste</ContextMenuItem>
        </ContextMenuContent>
      </ContextMenu>
      <ContextMenu>
        <ContextMenuTrigger
          holdFeedback={false}
          className="flex h-40 w-full items-center justify-center rounded-xl border border-dashed text-sm text-muted-foreground"
        >
          No hold feedback
        </ContextMenuTrigger>
        <ContextMenuContent>
          <ContextMenuItem>Copy</ContextMenuItem>
          <ContextMenuItem>Paste</ContextMenuItem>
        </ContextMenuContent>
      </ContextMenu>
    </div>
  )
}
```

### Long content

Long labels wrap inside a 20rem maximum width, and tall menus scroll within the space left in the viewport.

```tsx title="components/examples/context-menu/long-content.tsx"
import { IconFile } from "@tabler/icons-react"

import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuGroup,
  ContextMenuItem,
  ContextMenuLabel,
  ContextMenuShortcut,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"

export function ContextMenuLongContent() {
  return (
    <div className="grid w-full max-w-lg gap-3 sm:grid-cols-2">
      <ContextMenu>
        <ContextMenuTrigger className="flex h-40 w-full items-center justify-center rounded-xl border border-dashed text-sm text-muted-foreground">
          Long labels
        </ContextMenuTrigger>
        <ContextMenuContent>
          <ContextMenuLabel>
            A label that goes on for much longer than any menu label should
          </ContextMenuLabel>
          <ContextMenuItem>
            <IconFile />
            Move to “Supercalifragilisticexpialidocious project archive 2026”
            <ContextMenuShortcut>⌥⌘M</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuItem>
            https://example.com/a/really/long/url/without/any/spaces/at/all/in/it
          </ContextMenuItem>
          <ContextMenuItem>مرحبا بالعالم — mixed العربية text</ContextMenuItem>
          <ContextMenuItem>👩‍👩‍👧‍👦 Family 日本語のテキスト</ContextMenuItem>
          <ContextMenuItem>Short</ContextMenuItem>
        </ContextMenuContent>
      </ContextMenu>
      <ContextMenu>
        <ContextMenuTrigger className="flex h-40 w-full items-center justify-center rounded-xl border border-dashed text-sm text-muted-foreground">
          40 items
        </ContextMenuTrigger>
        <ContextMenuContent>
          <ContextMenuGroup>
            <ContextMenuLabel>Recent</ContextMenuLabel>
            {Array.from({ length: 40 }, (_, index) => (
              <ContextMenuItem key={index}>
                Document {index + 1}
              </ContextMenuItem>
            ))}
          </ContextMenuGroup>
        </ContextMenuContent>
      </ContextMenu>
    </div>
  )
}
```

### Nested submenus

Submenus open on hover or with the arrow keys, at any depth. A disabled submenu trigger never opens.

```tsx title="components/examples/context-menu/nested.tsx"
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"

export function ContextMenuNested() {
  return (
    <ContextMenu>
      <ContextMenuTrigger className="flex h-40 w-full max-w-sm items-center justify-center rounded-xl border border-dashed text-sm text-muted-foreground">
        Three levels deep
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem>Open</ContextMenuItem>
        <ContextMenuSub>
          <ContextMenuSubTrigger>Move to</ContextMenuSubTrigger>
          <ContextMenuSubContent>
            <ContextMenuItem>Inbox</ContextMenuItem>
            <ContextMenuSub>
              <ContextMenuSubTrigger>Projects</ContextMenuSubTrigger>
              <ContextMenuSubContent>
                <ContextMenuItem>Acme</ContextMenuItem>
                <ContextMenuSub>
                  <ContextMenuSubTrigger>Archive</ContextMenuSubTrigger>
                  <ContextMenuSubContent>
                    <ContextMenuItem>2024</ContextMenuItem>
                    <ContextMenuItem>2025</ContextMenuItem>
                  </ContextMenuSubContent>
                </ContextMenuSub>
              </ContextMenuSubContent>
            </ContextMenuSub>
            <ContextMenuSub disabled>
              <ContextMenuSubTrigger disabled>Locked</ContextMenuSubTrigger>
              <ContextMenuSubContent>
                <ContextMenuItem>Hidden</ContextMenuItem>
              </ContextMenuSubContent>
            </ContextMenuSub>
          </ContextMenuSubContent>
        </ContextMenuSub>
        <ContextMenuItem variant="destructive">Delete</ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

### Inside a sheet

The menu layers above other overlays, and Escape closes only the menu, not the sheet behind it.

```tsx title="components/examples/context-menu/in-sheet.tsx"
import { Button } from "@/components/ui/button"
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
import {
  Sheet,
  SheetBody,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function ContextMenuInSheet() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Open sheet
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Inside a sheet</SheetTitle>
          <SheetDescription>
            The menu layers above the sheet and Escape closes only the menu.
          </SheetDescription>
        </SheetHeader>
        <SheetBody>
          <ContextMenu>
            <ContextMenuTrigger className="flex h-40 w-full items-center justify-center rounded-xl border border-dashed text-sm text-muted-foreground">
              Right click here
            </ContextMenuTrigger>
            <ContextMenuContent>
              <ContextMenuItem>Copy</ContextMenuItem>
              <ContextMenuItem>Paste</ContextMenuItem>
            </ContextMenuContent>
          </ContextMenu>
        </SheetBody>
      </SheetContent>
    </Sheet>
  )
}
```

### Render as another element

Use `render` to make the trigger any element, such as a figure, or to turn an item into a link.

```tsx title="components/examples/context-menu/render.tsx"
import { IconDownload, IconLink } from "@tabler/icons-react"

import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"

export function ContextMenuRender() {
  return (
    <ContextMenu>
      <ContextMenuTrigger
        render={<figure />}
        className="flex w-full max-w-xs flex-col gap-2 rounded-xl border p-3"
      >
        <div className="h-28 rounded-lg bg-muted" />
        <figcaption className="text-sm text-muted-foreground">
          Trigger rendered as a figure
        </figcaption>
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem>
          <IconDownload />
          Save image
        </ContextMenuItem>
        <ContextMenuItem render={<a href="#render-as-another-element" />}>
          <IconLink />
          Link item
        </ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

### Right to left

The menu reads the trigger’s direction, so submenus open to the left and arrow keys flip.

```tsx title="components/examples/context-menu/rtl.tsx"
import { IconArrowLeft, IconArrowRight, IconTrash } from "@tabler/icons-react"

import {
  ContextMenu,
  ContextMenuCheckboxItem,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"

export function ContextMenuRtl() {
  return (
    <div dir="rtl" className="w-full max-w-sm">
      <ContextMenu>
        <ContextMenuTrigger className="flex h-40 w-full items-center justify-center rounded-xl border border-dashed text-sm text-muted-foreground">
          انقر بزر الفأرة الأيمن هنا
        </ContextMenuTrigger>
        <ContextMenuContent>
          <ContextMenuItem>
            <IconArrowRight />
            رجوع
            <ContextMenuShortcut>⌘[</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuItem>
            <IconArrowLeft />
            تقدم
          </ContextMenuItem>
          <ContextMenuCheckboxItem defaultChecked>
            إظهار الشريط
          </ContextMenuCheckboxItem>
          <ContextMenuSub>
            <ContextMenuSubTrigger>مشاركة</ContextMenuSubTrigger>
            <ContextMenuSubContent>
              <ContextMenuItem>بريد</ContextMenuItem>
              <ContextMenuItem>نسخ الرابط</ContextMenuItem>
            </ContextMenuSubContent>
          </ContextMenuSub>
          <ContextMenuSeparator />
          <ContextMenuItem variant="destructive">
            <IconTrash />
            حذف
          </ContextMenuItem>
        </ContextMenuContent>
      </ContextMenu>
    </div>
  )
}
```

## Keyboard

| Key | Action |
| --- | --- |
| `↓` | Highlights the next item, wrapping at the end. |
| `↑` | Highlights the previous item, wrapping at the start. |
| `Home` | Highlights the first item. |
| `End` | Highlights the last item. |
| `Enter` `Space` | Runs the highlighted item. Checkbox and radio items toggle and keep the menu open. |
| `→` | Opens the highlighted submenu and moves into it. ← in right-to-left layouts. |
| `←` | Closes the current submenu and returns to its trigger. → in right-to-left layouts. |
| `Esc` | Closes the current menu. In a submenu, only that submenu closes. |
| `A–Z` | Highlights the next item starting with that letter. |

## Accessibility

- A context menu is a shortcut. Make every action in it reachable some other way too, such as a visible button or a dropdown menu, since many people never right click or long press.
- Browsers also fire the context menu event for Shift F10 and the Menu key on a focused element, so a focusable element inside the trigger lets keyboard users open it.
- Shortcuts in `<ContextMenuShortcut />` are labels only. Bind the keys yourself.
- With reduced motion on, the hold feedback and the item blink are skipped and the menu only fades.

## API reference

Built on the Base UI context menu. Every part accepts the props of the primitive it wraps, and a function `className` that receives the part’s state.

### ContextMenu

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | – |  |
| `defaultOpen` | `boolean` | `false` |  |
| `onOpenChange` | `(open: boolean, details) => void` | – | details.reason says what caused the change. |
| `onOpenChangeComplete` | `(open: boolean) => void` | – | Runs after the open or close animation ends. |
| `disabled` | `boolean` | `false` | Shows the browser’s native menu instead. |
| `loopFocus` | `boolean` | `true` | Wrap arrow-key navigation at the ends. |
| `highlightItemOnHover` | `boolean` | `true` |  |
| `actionsRef` | `RefObject<{ close, unmount }>` | – | Close the menu imperatively. |

### ContextMenuTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `holdFeedback` | `boolean` | `true` | Shrink the area slightly while a long press is held on touch screens. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="context-menu-trigger"` | Target the trigger in CSS. |
| `data-popup-open` | Present while its menu is open. |
| `data-holding` | Present while a long press is held. |
| `data-pressed` | Present while the trigger is pressed. |

### ContextMenuContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side` | `"top" \| "right" \| "bottom" \| "left" \| "inline-start" \| "inline-end"` | `"bottom"` | Preferred side, relative to the pointer. |
| `align` | `"start" \| "center" \| "end"` | `"start"` |  |
| `sideOffset` | `number` | `0` |  |
| `alignOffset` | `number` | `0` |  |
| `collisionPadding` | `number \| { top, right, bottom, left }` | – | Space to keep between the menu and the viewport edges. |
| `collisionAvoidance` | `CollisionAvoidance` | – | How the menu flips or shifts when it would overflow. |
| `anchor` | `Element \| VirtualElement \| RefObject` | – | Position against something other than the pointer. |
| `finalFocus` | `boolean \| RefObject \| (closeType) => HTMLElement \| boolean` | – | Where focus goes after the menu closes. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="context-menu-content"` | The menu popup. |
| `data-open` | Present while open. |
| `data-starting-style` | Present while the menu animates in. |
| `data-ending-style` | Present while the menu animates out. |
| `data-side` | The side it was placed on after collisions. |
| `data-chosen` | Present after an item is clicked. The fade-out waits for the blink. |
| `--transform-origin` | The point the scale animation grows from. |
| `--available-height` | Space left in the viewport. Caps the menu height. |

### ContextMenuItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "destructive"` | `"default"` |  |
| `inset` | `boolean` | `false` | Indent the label to line up with checkbox items. |
| `onClick` | `(event) => void` | – | Runs on click, Enter or Space. The menu closes after a short blink. |
| `closeOnClick` | `boolean` | `true` |  |
| `disabled` | `boolean` | `false` |  |
| `label` | `string` | – | Text used for typeahead when children are not plain text. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="context-menu-item"` | Target items in CSS. |
| `data-variant` | The current variant. |
| `data-highlighted` | Present on the item under the pointer or keyboard focus. |
| `data-disabled` | Present when the item is disabled. |
| `data-inset` | Present when inset is set. |
| `data-chosen` | Present on the clicked item while it blinks. |

### ContextMenuCheckboxItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `checked` | `boolean` | – |  |
| `defaultChecked` | `boolean` | `false` |  |
| `onCheckedChange` | `(checked: boolean, details) => void` | – |  |
| `closeOnClick` | `boolean` | `false` |  |
| `inset` | `boolean` | `false` |  |
| `disabled` | `boolean` | `false` |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="context-menu-checkbox-item"` | Target checkbox items in CSS. |
| `data-checked` | Present when checked. |
| `data-unchecked` | Present when unchecked. |
| `data-highlighted` | Present on the item under the pointer or keyboard focus. |
| `data-disabled` | Present when the item is disabled. |
| `data-inset` | Present when inset is set. |
| `data-chosen` | Present on the clicked item while it blinks. |

### ContextMenuRadioGroup

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `any` | – |  |
| `defaultValue` | `any` | – |  |
| `onValueChange` | `(value: any, details) => void` | – |  |
| `disabled` | `boolean` | `false` |  |

### ContextMenuRadioItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `any` | – |  |
| `closeOnClick` | `boolean` | `false` |  |
| `inset` | `boolean` | `false` |  |
| `disabled` | `boolean` | `false` |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="context-menu-radio-item"` | Target radio items in CSS. |
| `data-checked` | Present when selected. |
| `data-highlighted` | Present on the item under the pointer or keyboard focus. |
| `data-disabled` | Present when the item is disabled. |
| `data-inset` | Present when inset is set. |
| `data-chosen` | Present on the clicked item while it blinks. |

### ContextMenuLabel

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `inset` | `boolean` | `false` |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

Inside `<ContextMenuGroup />` or `<ContextMenuRadioGroup />` it labels the group for assistive tech. Elsewhere it is a plain heading.

### ContextMenuSub

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | – |  |
| `defaultOpen` | `boolean` | `false` |  |
| `onOpenChange` | `(open: boolean, details) => void` | – |  |
| `disabled` | `boolean` | `false` |  |
| `closeParentOnEsc` | `boolean` | `false` | Close the whole menu on Escape, not just this submenu. |

### ContextMenuSubTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `inset` | `boolean` | `false` |  |
| `openOnHover` | `boolean` | `true` |  |
| `delay` | `number` | `100` | Milliseconds of hover before the submenu opens. |
| `closeDelay` | `number` | `0` |  |
| `disabled` | `boolean` | `false` |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="context-menu-sub-trigger"` | Target submenu triggers in CSS. |
| `data-popup-open` | Present while its submenu is open. |
| `data-highlighted` | Present while highlighted. |
| `data-disabled` | Present when disabled. |

### ContextMenuSubContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side` | `"top" \| "right" \| "bottom" \| "left" \| "inline-start" \| "inline-end"` | – | Opens toward the inline end by default. |
| `align` | `"start" \| "center" \| "end"` | – |  |
| `sideOffset` | `number` | `0` |  |
| `alignOffset` | `number` | `-4` |  |
| `collisionPadding` | `number \| { top, right, bottom, left }` | – | Space to keep between the menu and the viewport edges. |
| `collisionAvoidance` | `CollisionAvoidance` | – | How the menu flips or shifts when it would overflow. |
| `anchor` | `Element \| VirtualElement \| RefObject` | – | Position against something other than the pointer. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="context-menu-sub-content"` | The submenu popup. Takes the same state attributes as the menu. |

### ContextMenuShortcut

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `dir` | `"ltr" \| "rtl"` | `"ltr"` | Keeps shortcuts like ⇧⌘S in order inside right-to-left menus. |

| Attribute | Description |
| --- | --- |
| `data-slot="context-menu-shortcut"` | The shortcut label. |

### ContextMenuGroup and ContextMenuSeparator

`<ContextMenuGroup />` groups related items under a label. `<ContextMenuSeparator />` draws a divider. Both accept `render` and `className`, and carry `context-menu-group` and `context-menu-separator` as their `data-slot`.

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
