# Context Menu

> Show actions at a pointer location with keyboard and touch access.

Source: https://www.honestui.com/docs/components/context-menu

---
title: Context Menu
description: Show actions at a pointer location with keyboard and touch access.

links:
  doc: https://base-ui.com/react/components/context-menu#api-reference
---

```tsx
"use client"

import * as React from "react"
import {
  Archive as ArchiveIcon,
  Copy as CopyIcon,
  Folder as FolderIcon,
  Pencil as PencilIcon,
  Trash as TrashIcon,
} from "honestui/icons"

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

export function ContextMenuDemo() {
  const [lastAction, setLastAction] = React.useState("No action selected")

  return (
    <div className="w-full max-w-md space-y-3">
      <ContextMenu>
        <ContextMenuTrigger
          className="flex min-h-48 w-full items-center justify-center rounded-lg border border-dashed border-border bg-muted/30 p-8 text-center text-sm text-muted-foreground"
          tabIndex={0}
        >
          <span>
            Right-click or long-press this area.
            <span className="mt-1 block text-xs">
              With a keyboard, focus it and press Shift+F10.
            </span>
          </span>
        </ContextMenuTrigger>
        <ContextMenuContent>
          <ContextMenuGroup>
            <ContextMenuLabel>Document</ContextMenuLabel>
            <ContextMenuItem
              leadingIcon={<CopyIcon />}
              onClick={() => setLastAction("Copied document link")}
            >
              Copy link
            </ContextMenuItem>
            <ContextMenuItem
              leadingIcon={<PencilIcon />}
              onClick={() => setLastAction("Rename selected")}
            >
              Rename
            </ContextMenuItem>
          </ContextMenuGroup>
          <ContextMenuSeparator />
          <ContextMenuSub>
            <ContextMenuSubTrigger leadingIcon={<FolderIcon />}>
              Move to
            </ContextMenuSubTrigger>
            <ContextMenuSubContent>
              <ContextMenuItem
                onClick={() => setLastAction("Moved to Projects")}
              >
                Projects
              </ContextMenuItem>
              <ContextMenuItem
                onClick={() => setLastAction("Moved to Research")}
              >
                Research
              </ContextMenuItem>
              <ContextMenuItem
                leadingIcon={<ArchiveIcon />}
                onClick={() => setLastAction("Moved to Archive")}
              >
                Archive
              </ContextMenuItem>
            </ContextMenuSubContent>
          </ContextMenuSub>
          <ContextMenuSeparator />
          <ContextMenuItem
            leadingIcon={<TrashIcon />}
            variant="destructive"
            onClick={() => setLastAction("Delete selected")}
          >
            Delete
          </ContextMenuItem>
        </ContextMenuContent>
      </ContextMenu>
      <p className="text-center text-xs text-muted-foreground" aria-live="polite">
        {lastAction}
      </p>
    </div>
  )
}

```

## Overview

Use Context Menu for secondary actions that relate to a specific surface, document, or selected object. It opens where a person right-clicks or long-presses, keeping the available actions close to the object they affect.

Do not make a context menu the only way to reach an essential action. Many people will not discover it, and some input methods do not provide a familiar context-menu gesture. Keep primary and high-frequency actions visible elsewhere in the interface.

## Anatomy

Context Menu includes a root, trigger region, content, items, optional groups and labels, separators, an empty state, and nested submenus. Every part is defined in the single `context-menu.tsx` file and exported as a named PascalCase component, matching the rest of Honest UI.

## Behavior

The trigger opens the menu on right-click, long-press, or the platform context-menu keyboard command. Items close the menu after selection. Submenus open from a submenu trigger and preserve arrow-key navigation.

Set `autocomplete` on the root when a genuinely long command list benefits from filtering. In automatic mode, item `value` strings and text labels are matched against the query. Labels and separators are hidden while filtering so the results remain a single navigable list. Use `autocompleteMode="manual"` when your application owns filtering.

## Accessibility

Give a non-interactive trigger region `tabIndex={0}` so keyboard users can focus it and open the menu with Shift+F10 or the Menu key. The same actions must also be available through a visible control when they are required to complete a task.

Use concise action labels and mark unavailable items as disabled instead of removing them when their absence would be confusing. The searchable variant gives its input an explicit accessible label through `searchLabel`; change that label when “Search menu items” is not specific enough. Base UI supplies menu roles, focus management, arrow-key movement, Escape behavior, and long-press handling.

## Installation






```bash
npx honestui@latest add context-menu
```







Install the required dependency:

```bash
npm install @base-ui/react
```

Copy and paste the component into your project.

### components/ui/context-menu.tsx

```tsx
"use client"

import * as React from "react"
import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete"
import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"
import { ChevronRight as ChevronRightIcon } from "honestui/icons"

import { cn } from "@/lib/utils"

type AutocompleteMode = "auto" | "manual"

type ContextMenuContextValue = {
  autocomplete: boolean
  autocompleteMode: AutocompleteMode
  inputRef: React.RefObject<HTMLInputElement | null>
  inputValue: string
  isInitialRenderRef: React.RefObject<boolean>
  onInputValueChange: (value: string) => void
  parent?: ContextMenuContextValue
  shouldFilter: boolean
}

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

function useContextMenuContext() {
  const context = React.useContext(ContextMenuContext)

  if (!context) {
    throw new Error("Context menu parts must be used within ContextMenu")
  }

  return context
}

function getTextContent(node: React.ReactNode): string {
  if (typeof node === "string" || typeof node === "number") {
    return String(node)
  }

  if (Array.isArray(node)) {
    return node.map(getTextContent).join(" ")
  }

  if (React.isValidElement<{ children?: React.ReactNode }>(node)) {
    return getTextContent(node.props.children)
  }

  return ""
}

function matchesSearch(
  value: string | undefined,
  children: React.ReactNode,
  query: string
) {
  const normalizedQuery = query.trim().toLocaleLowerCase()

  if (!normalizedQuery) return true

  return (value ?? getTextContent(children))
    .toLocaleLowerCase()
    .includes(normalizedQuery)
}

function dispatchKeyboardEvent(element: Element, key: string) {
  element.dispatchEvent(
    new KeyboardEvent("keydown", {
      bubbles: true,
      cancelable: true,
      key,
    })
  )
}

function isSubmenuTrigger(element: Element) {
  return element.matches('[data-slot="context-menu-subtrigger"]')
}

function isSubmenuOpen(element: Element) {
  return element.hasAttribute("data-popup-open")
}

interface NormalContextMenuRootProps
  extends ContextMenuPrimitive.Root.Props {
  autocomplete?: false
  autocompleteMode?: never
  defaultInputValue?: never
  inputValue?: never
  onInputValueChange?: never
}

interface AutocompleteContextMenuRootProps
  extends ContextMenuPrimitive.Root.Props {
  autocomplete: true
  autocompleteMode?: AutocompleteMode
  defaultInputValue?: string
  inputValue?: string
  onInputValueChange?: (value: string) => void
}

type ContextMenuRootProps =
  | NormalContextMenuRootProps
  | AutocompleteContextMenuRootProps

function ContextMenu({
  autocomplete = false,
  autocompleteMode = "auto",
  inputValue: controlledInputValue,
  onInputValueChange,
  defaultInputValue = "",
  open: controlledOpen,
  onOpenChange,
  defaultOpen = false,
  ...props
}: ContextMenuRootProps) {
  const [uncontrolledInputValue, setUncontrolledInputValue] =
    React.useState(defaultInputValue)
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
  const inputRef = React.useRef<HTMLInputElement>(null)
  const isInitialRenderRef = React.useRef(true)
  const inputValue = controlledInputValue ?? uncontrolledInputValue
  const open = controlledOpen ?? uncontrolledOpen

  const setInputValue = React.useCallback(
    (value: string) => {
      if (controlledInputValue === undefined) {
        setUncontrolledInputValue(value)
      }
      onInputValueChange?.(value)
    },
    [controlledInputValue, onInputValueChange]
  )

  const handleOpenChange = React.useCallback<
    NonNullable<ContextMenuPrimitive.Root.Props["onOpenChange"]>
  >(
    (nextOpen, eventDetails) => {
      if (controlledOpen === undefined) {
        setUncontrolledOpen(nextOpen)
      }

      if (!nextOpen && autocomplete) {
        setInputValue("")
        isInitialRenderRef.current = true
      }

      onOpenChange?.(nextOpen, eventDetails)
    },
    [autocomplete, controlledOpen, onOpenChange, setInputValue]
  )

  const context = React.useMemo<ContextMenuContextValue>(
    () => ({
      autocomplete,
      autocompleteMode,
      inputRef,
      inputValue,
      isInitialRenderRef,
      onInputValueChange: setInputValue,
      shouldFilter: autocomplete && autocompleteMode === "auto",
    }),
    [autocomplete, autocompleteMode, inputValue, setInputValue]
  )

  return (
    <ContextMenuContext.Provider value={context}>
      <ContextMenuPrimitive.Root
        open={open}
        onOpenChange={handleOpenChange}
        loopFocus={false}
        {...props}
      />
    </ContextMenuContext.Provider>
  )
}
interface NormalContextMenuSubProps
  extends ContextMenuPrimitive.SubmenuRoot.Props {
  autocomplete?: false
  autocompleteMode?: never
  defaultInputValue?: never
  inputValue?: never
  onInputValueChange?: never
}

interface AutocompleteContextMenuSubProps
  extends ContextMenuPrimitive.SubmenuRoot.Props {
  autocomplete: true
  autocompleteMode?: AutocompleteMode
  defaultInputValue?: string
  inputValue?: string
  onInputValueChange?: (value: string) => void
}

type ContextMenuSubProps =
  | NormalContextMenuSubProps
  | AutocompleteContextMenuSubProps

function ContextMenuSub({
  autocomplete = false,
  autocompleteMode = "auto",
  inputValue: controlledInputValue,
  onInputValueChange,
  defaultInputValue = "",
  open: controlledOpen,
  onOpenChange,
  defaultOpen = false,
  ...props
}: ContextMenuSubProps) {
  const parent = useContextMenuContext()
  const [uncontrolledInputValue, setUncontrolledInputValue] =
    React.useState(defaultInputValue)
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
  const inputRef = React.useRef<HTMLInputElement>(null)
  const isInitialRenderRef = React.useRef(true)
  const inputValue = controlledInputValue ?? uncontrolledInputValue
  const open = controlledOpen ?? uncontrolledOpen

  const setInputValue = React.useCallback(
    (value: string) => {
      if (controlledInputValue === undefined) {
        setUncontrolledInputValue(value)
      }
      onInputValueChange?.(value)
    },
    [controlledInputValue, onInputValueChange]
  )

  const handleOpenChange = React.useCallback<
    NonNullable<ContextMenuPrimitive.SubmenuRoot.Props["onOpenChange"]>
  >(
    (nextOpen, eventDetails) => {
      if (controlledOpen === undefined) {
        setUncontrolledOpen(nextOpen)
      }

      if (!nextOpen && autocomplete) {
        setInputValue("")
        isInitialRenderRef.current = true
      }

      onOpenChange?.(nextOpen, eventDetails)
    },
    [autocomplete, controlledOpen, onOpenChange, setInputValue]
  )

  const context = React.useMemo<ContextMenuContextValue>(
    () => ({
      autocomplete,
      autocompleteMode,
      inputRef,
      inputValue,
      isInitialRenderRef,
      onInputValueChange: setInputValue,
      parent,
      shouldFilter: autocomplete && autocompleteMode === "auto",
    }),
    [
      autocomplete,
      autocompleteMode,
      inputValue,
      parent,
      setInputValue,
    ]
  )

  return (
    <ContextMenuContext.Provider value={context}>
      <ContextMenuPrimitive.SubmenuRoot
        open={open}
        onOpenChange={handleOpenChange}
        loopFocus={false}
        {...props}
      />
    </ContextMenuContext.Provider>
  )
}
type ContextMenuTriggerProps = ContextMenuPrimitive.Trigger.Props

function ContextMenuTrigger({ className, ...props }: ContextMenuTriggerProps) {
  return (
    <ContextMenuPrimitive.Trigger
      data-slot="context-menu-trigger"
      className={cn(
        "outline-none focus-visible:ring-2 focus-visible:ring-[var(--hui-color-border-focus)] focus-visible:ring-offset-2",
        className
      )}
      {...props}
    />
  )
}
type ContextMenuCellProps = React.ComponentProps<"div"> & {
  inset?: boolean
  leadingIcon?: React.ReactNode
  trailingIcon?: React.ReactNode
  variant?: "default" | "destructive"
}

function ContextMenuCell({
  children,
  className,
  inset,
  leadingIcon,
  trailingIcon,
  variant = "default",
  ...props
}: ContextMenuCellProps) {
  return (
    <div
      data-inset={inset || undefined}
      data-variant={variant}
      className={cn(
        "relative flex items-center gap-[var(--hui-space-3)] p-[var(--hui-space-3)] outline-none select-none [font-size:var(--hui-font-size-small)] [font-weight:var(--hui-font-weight-regular)] [letter-spacing:var(--hui-letter-spacing-small)] [line-height:var(--hui-line-height-small)] aria-disabled:pointer-events-none aria-disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:cursor-pointer data-highlighted:rounded-[var(--hui-radius-2)] data-highlighted:bg-[var(--hui-color-background-base-primary-hover)] data-inset:ps-8 data-popup-open:cursor-pointer data-popup-open:rounded-[var(--hui-radius-2)] data-popup-open:bg-[var(--hui-color-background-base-primary-hover)] data-[variant=destructive]:text-[var(--hui-color-foreground-danger-primary)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:text-[var(--hui-color-foreground-base-secondary)] [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    >
      {leadingIcon ? <span aria-hidden="true">{leadingIcon}</span> : null}
      {children}
      {trailingIcon ? (
        <span className="ms-auto" aria-hidden="true">
          {trailingIcon}
        </span>
      ) : null}
    </div>
  )
}

type ContextMenuItemProps = ContextMenuPrimitive.Item.Props & {
  inset?: boolean
  leadingIcon?: React.ReactNode
  trailingIcon?: React.ReactNode
  value?: string
  variant?: "default" | "destructive"
}

function ContextMenuItem({
  children,
  value,
  leadingIcon,
  trailingIcon,
  inset,
  variant,
  render,
  ...props
}: ContextMenuItemProps) {
  const { autocomplete, inputValue, shouldFilter } = useContextMenuContext()

  if (shouldFilter && !matchesSearch(value, children, inputValue)) {
    return null
  }

  const cell =
    render ?? (
      <ContextMenuCell
        inset={inset}
        leadingIcon={leadingIcon}
        trailingIcon={trailingIcon}
        variant={variant}
      />
    )

  if (autocomplete) {
    return (
      <AutocompletePrimitive.Item
        data-slot="context-menu-item"
        value={value ?? getTextContent(children)}
        render={<ContextMenuPrimitive.Item render={cell} />}
        {...props}
      >
        {children}
      </AutocompletePrimitive.Item>
    )
  }

  return (
    <ContextMenuPrimitive.Item
      data-slot="context-menu-item"
      render={cell}
      {...props}
    >
      {children}
    </ContextMenuPrimitive.Item>
  )
}
type ContextMenuSubTriggerProps =
  ContextMenuPrimitive.SubmenuTrigger.Props & {
    inset?: boolean
    leadingIcon?: React.ReactNode
    trailingIcon?: React.ReactNode
    value?: string
  }

function ContextMenuSubTrigger({
  children,
  value,
  trailingIcon = <ChevronRightIcon />,
  leadingIcon,
  inset,
  ...props
}: ContextMenuSubTriggerProps) {
  const { inputRef, parent } = useContextMenuContext()

  if (
    parent?.shouldFilter &&
    !matchesSearch(value, children, parent.inputValue)
  ) {
    return null
  }

  const cell = (
    <ContextMenuCell
      inset={inset}
      leadingIcon={leadingIcon}
      trailingIcon={trailingIcon}
    />
  )

  return (
    <ContextMenuPrimitive.SubmenuTrigger
      data-slot="context-menu-subtrigger"
      render={
        parent?.autocomplete ? (
          <AutocompletePrimitive.Item
            value={value ?? getTextContent(children)}
            render={cell}
            onPointerEnter={(event) => {
              if (document.activeElement !== parent.inputRef.current) {
                parent.inputRef.current?.focus()
              }
              props.onPointerEnter?.(event)
            }}
            onKeyDown={(event) => {
              requestAnimationFrame(() => inputRef.current?.focus())
              props.onKeyDown?.(event)
            }}
          />
        ) : (
          cell
        )
      }
      role={parent?.autocomplete ? "option" : "menuitem"}
      {...props}
    >
      {children}
    </ContextMenuPrimitive.SubmenuTrigger>
  )
}
type ContextMenuGroupProps = ContextMenuPrimitive.Group.Props

function ContextMenuGroup({ children, ...props }: ContextMenuGroupProps) {
  const { shouldFilter } = useContextMenuContext()

  if (shouldFilter) return <>{children}</>

  return (
    <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props}>
      {children}
    </ContextMenuPrimitive.Group>
  )
}
type ContextMenuLabelProps = ContextMenuPrimitive.GroupLabel.Props

function ContextMenuLabel({ className, ...props }: ContextMenuLabelProps) {
  const { shouldFilter } = useContextMenuContext()

  if (shouldFilter) return null

  return (
    <ContextMenuPrimitive.GroupLabel
      data-slot="context-menu-label"
      className={cn(
        "px-[var(--hui-space-3)] py-[var(--hui-space-2)] [font-size:var(--hui-font-size-mini)] [font-weight:var(--hui-font-weight-medium)]",
        className
      )}
      {...props}
    />
  )
}
type ContextMenuSeparatorProps = ContextMenuPrimitive.Separator.Props

function ContextMenuSeparator({
  className,
  ...props
}: ContextMenuSeparatorProps) {
  const { shouldFilter } = useContextMenuContext()

  if (shouldFilter) return null

  return (
    <ContextMenuPrimitive.Separator
      data-slot="context-menu-separator"
      className={cn(
        "my-[var(--hui-space-2)] h-px bg-[var(--hui-color-border-base-primary)] [margin-inline:calc(var(--hui-space-3)*-1)]",
        className
      )}
      {...props}
    />
  )
}
function ContextMenuEmptyState({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="context-menu-empty-state"
      className={cn(
        "hidden p-[var(--hui-space-3)] text-center text-[var(--hui-color-foreground-base-secondary)] [font-size:var(--hui-font-size-small)] only:block",
        className
      )}
      {...props}
    />
  )
}
type ContextMenuContentProps = ContextMenuPrimitive.Popup.Props & {
  align?: ContextMenuPrimitive.Positioner.Props["align"]
  alignOffset?: ContextMenuPrimitive.Positioner.Props["alignOffset"]
  searchLabel?: string
  searchPlaceholder?: string
  sideOffset?: ContextMenuPrimitive.Positioner.Props["sideOffset"]
}

function ContextMenuContent({
  ref,
  className,
  children,
  searchLabel = "Search menu items",
  searchPlaceholder = "Search...",
  sideOffset = 4,
  align = "start",
  alignOffset = 0,
  onFocus,
  ...props
}: ContextMenuContentProps) {
  const {
    autocomplete,
    inputValue,
    inputRef,
    isInitialRenderRef,
    onInputValueChange,
    parent,
  } = useContextMenuContext()
  const highlightedItem = React.useRef<
    [index: number, reason: "keyboard" | "pointer" | "none"]
  >([-1, "none"])
  const listRef = React.useRef<HTMLDivElement>(null)

  const focusInput = React.useCallback(() => {
    if (document.activeElement !== inputRef.current) {
      inputRef.current?.focus()
    }
  }, [inputRef])

  const highlightFirstItem = React.useCallback(() => {
    if (!isInitialRenderRef.current) return
    isInitialRenderRef.current = false

    const item = listRef.current?.querySelector('[role="option"]')
    item?.dispatchEvent(new PointerEvent("mousemove", { bubbles: true }))
  }, [isInitialRenderRef])

  const openHighlightedSubmenu = React.useCallback(() => {
    if (highlightedItem.current[0] === -1) return

    const items = listRef.current?.querySelectorAll('[role="option"]')
    const item = items?.[highlightedItem.current[0]]

    if (item && isSubmenuTrigger(item)) {
      dispatchKeyboardEvent(item, "ArrowRight")
    }
  }, [])

  const closeHighlightedSubmenu = React.useCallback(
    (event: React.KeyboardEvent<HTMLInputElement>) => {
      if (highlightedItem.current[0] === -1) return

      const items = listRef.current?.querySelectorAll('[role="option"]')
      const item = items?.[highlightedItem.current[0]]

      if (!item || !isSubmenuTrigger(item) || !isSubmenuOpen(item)) return

      dispatchKeyboardEvent(item, "Escape")
      event.stopPropagation()
    },
    []
  )

  const closeStaleSubmenu = React.useCallback((index: number) => {
    const items = listRef.current?.querySelectorAll('[role="option"]')
    const item = items?.[index]

    if (!item || !isSubmenuTrigger(item) || !isSubmenuOpen(item)) return

    dispatchKeyboardEvent(item, "Escape")
    item.dispatchEvent(new PointerEvent("pointerout", { bubbles: true }))
  }, [])

  return (
    <ContextMenuPrimitive.Portal>
      <ContextMenuPrimitive.Positioner
        data-slot="context-menu-positioner"
        className="z-[var(--hui-z-index-portal)]"
        sideOffset={sideOffset}
        align={align}
        alignOffset={alignOffset}
      >
        <ContextMenuPrimitive.Popup
          ref={ref}
          data-slot="context-menu-content"
          className={cn(
            "box-border max-h-(--available-height) min-w-48 origin-(--transform-origin) overflow-x-hidden overflow-y-auto overscroll-contain rounded-[var(--hui-radius-2)] border-[0.5px] border-[var(--hui-color-border-base-primary)] bg-[var(--hui-color-background-base-primary)] p-[var(--hui-space-2)] text-[var(--hui-color-foreground-base-primary)] shadow-[var(--hui-shadow-soft)] outline-none [font-size:var(--hui-font-size-small)] [font-weight:var(--hui-font-weight-regular)] [letter-spacing:var(--hui-letter-spacing-small)] [line-height:var(--hui-line-height-small)] [transition:opacity_var(--hui-duration-fast)_var(--hui-ease-out)] focus:outline-none focus-visible:outline-none data-ending-style:opacity-0 data-starting-style:opacity-0 motion-safe:[transition:opacity_var(--hui-duration-fast)_var(--hui-ease-out),transform_var(--hui-duration-fast)_var(--hui-ease-out)] motion-safe:data-ending-style:scale-[0.97] motion-safe:data-starting-style:scale-[0.97]",
            className
          )}
          role={autocomplete ? "dialog" : "menu"}
          onFocus={
            autocomplete || parent?.autocomplete
              ? (event) => {
                  focusInput()
                  event.stopPropagation()
                  highlightFirstItem()
                  onFocus?.(event)
                }
              : onFocus
          }
          {...props}
        >
          {autocomplete ? (
            <AutocompletePrimitive.Root
              inline
              open
              value={inputValue}
              onValueChange={(value) => onInputValueChange(value)}
              autoHighlight={Boolean(inputValue)}
              mode="none"
              loopFocus={false}
              onItemHighlighted={(_, eventDetails) => {
                if (
                  highlightedItem.current[1] === "pointer" &&
                  eventDetails.reason === "keyboard"
                ) {
                  closeStaleSubmenu(highlightedItem.current[0])
                }

                highlightedItem.current = [
                  eventDetails.index,
                  eventDetails.reason,
                ]
              }}
            >
              <AutocompletePrimitive.Input
                ref={inputRef}
                data-slot="context-menu-search-input"
                aria-label={searchLabel}
                placeholder={searchPlaceholder}
                className="mb-[var(--hui-space-2)] h-8 w-full rounded-[var(--hui-radius-2)] border-0 bg-transparent px-[var(--hui-space-3)] text-[var(--hui-color-foreground-base-primary)] shadow-none outline-none placeholder:text-[var(--hui-color-foreground-base-secondary)] focus-visible:bg-[var(--hui-color-background-base-primary-hover)] focus-visible:ring-0 focus-visible:outline-none forced-colors:focus-visible:outline forced-colors:focus-visible:outline-1 forced-colors:focus-visible:outline-offset-[-2px]"
                onPointerEnter={focusInput}
                onKeyDown={(event) => {
                  if (event.key === "ArrowLeft") return
                  if (event.key === "Escape") {
                    closeHighlightedSubmenu(event)
                    return
                  }
                  if (event.key === "ArrowRight" || event.key === "Enter") {
                    openHighlightedSubmenu()
                  }
                  event.stopPropagation()
                }}
                tabIndex={-1}
              />
              <AutocompletePrimitive.List
                ref={listRef}
                data-slot="context-menu-search-list"
              >
                {children}
              </AutocompletePrimitive.List>
            </AutocompletePrimitive.Root>
          ) : (
            children
          )}
        </ContextMenuPrimitive.Popup>
      </ContextMenuPrimitive.Positioner>
    </ContextMenuPrimitive.Portal>
  )
}
function ContextMenuSubContent(props: ContextMenuContentProps) {
  return <ContextMenuContent sideOffset={2} alignOffset={-4} {...props} />
}

export {
  ContextMenu,
  ContextMenuContent,
  ContextMenuEmptyState,
  ContextMenuGroup,
  ContextMenuItem,
  ContextMenuLabel,
  ContextMenuSeparator,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
}

export type {
  AutocompleteContextMenuRootProps,
  AutocompleteContextMenuSubProps,
  ContextMenuContentProps,
  ContextMenuGroupProps,
  ContextMenuItemProps,
  ContextMenuLabelProps,
  ContextMenuRootProps,
  ContextMenuSeparatorProps,
  ContextMenuSubProps,
  ContextMenuSubTriggerProps,
  ContextMenuTriggerProps,
  NormalContextMenuRootProps,
  NormalContextMenuSubProps,
}

```

Update the import paths to match your project setup.







## Usage

```tsx
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuGroup,
  ContextMenuItem,
  ContextMenuLabel,
  ContextMenuSeparator,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
} from "@/components/ui/context-menu";
```

```tsx
<ContextMenu>
  <ContextMenuTrigger tabIndex={0}>
    Right-click this document
  </ContextMenuTrigger>
  <ContextMenuContent>
    <ContextMenuGroup>
      <ContextMenuLabel>Document</ContextMenuLabel>
      <ContextMenuItem>Rename</ContextMenuItem>
      <ContextMenuItem>Duplicate</ContextMenuItem>
    </ContextMenuGroup>
    <ContextMenuSeparator />
    <ContextMenuSub>
      <ContextMenuSubTrigger>Move to</ContextMenuSubTrigger>
      <ContextMenuSubContent>
        <ContextMenuItem>Projects</ContextMenuItem>
        <ContextMenuItem>Archive</ContextMenuItem>
      </ContextMenuSubContent>
    </ContextMenuSub>
  </ContextMenuContent>
</ContextMenu>
```

## Examples

The first example shows grouped actions, a nested destination menu, destructive styling, and visible feedback after selection. The second adds inline search for a longer command list.

### Nested actions

```tsx
"use client"

import * as React from "react"
import {
  Archive as ArchiveIcon,
  Copy as CopyIcon,
  Folder as FolderIcon,
  Pencil as PencilIcon,
  Trash as TrashIcon,
} from "honestui/icons"

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

export function ContextMenuDemo() {
  const [lastAction, setLastAction] = React.useState("No action selected")

  return (
    <div className="w-full max-w-md space-y-3">
      <ContextMenu>
        <ContextMenuTrigger
          className="flex min-h-48 w-full items-center justify-center rounded-lg border border-dashed border-border bg-muted/30 p-8 text-center text-sm text-muted-foreground"
          tabIndex={0}
        >
          <span>
            Right-click or long-press this area.
            <span className="mt-1 block text-xs">
              With a keyboard, focus it and press Shift+F10.
            </span>
          </span>
        </ContextMenuTrigger>
        <ContextMenuContent>
          <ContextMenuGroup>
            <ContextMenuLabel>Document</ContextMenuLabel>
            <ContextMenuItem
              leadingIcon={<CopyIcon />}
              onClick={() => setLastAction("Copied document link")}
            >
              Copy link
            </ContextMenuItem>
            <ContextMenuItem
              leadingIcon={<PencilIcon />}
              onClick={() => setLastAction("Rename selected")}
            >
              Rename
            </ContextMenuItem>
          </ContextMenuGroup>
          <ContextMenuSeparator />
          <ContextMenuSub>
            <ContextMenuSubTrigger leadingIcon={<FolderIcon />}>
              Move to
            </ContextMenuSubTrigger>
            <ContextMenuSubContent>
              <ContextMenuItem
                onClick={() => setLastAction("Moved to Projects")}
              >
                Projects
              </ContextMenuItem>
              <ContextMenuItem
                onClick={() => setLastAction("Moved to Research")}
              >
                Research
              </ContextMenuItem>
              <ContextMenuItem
                leadingIcon={<ArchiveIcon />}
                onClick={() => setLastAction("Moved to Archive")}
              >
                Archive
              </ContextMenuItem>
            </ContextMenuSubContent>
          </ContextMenuSub>
          <ContextMenuSeparator />
          <ContextMenuItem
            leadingIcon={<TrashIcon />}
            variant="destructive"
            onClick={() => setLastAction("Delete selected")}
          >
            Delete
          </ContextMenuItem>
        </ContextMenuContent>
      </ContextMenu>
      <p className="text-center text-xs text-muted-foreground" aria-live="polite">
        {lastAction}
      </p>
    </div>
  )
}

```

### Searchable commands

```tsx
"use client"

import * as React from "react"

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

export function ContextMenuSearch() {
  const [lastAction, setLastAction] = React.useState("No command selected")

  return (
    <div className="w-full max-w-md space-y-3">
      <ContextMenu autocomplete>
        <ContextMenuTrigger
          className="flex min-h-48 w-full items-center justify-center rounded-lg border border-dashed border-border bg-muted/30 p-8 text-center text-sm text-muted-foreground"
          tabIndex={0}
        >
          <span>
            Open the context menu to search commands.
            <span className="mt-1 block text-xs">
              Right-click, long-press, or press Shift+F10.
            </span>
          </span>
        </ContextMenuTrigger>
        <ContextMenuContent
          searchLabel="Search document commands"
          searchPlaceholder="Search commands..."
        >
          <ContextMenuItem
            value="copy document link"
            onClick={() => setLastAction("Copied document link")}
          >
            Copy link
          </ContextMenuItem>
          <ContextMenuItem
            value="rename document"
            onClick={() => setLastAction("Rename selected")}
          >
            Rename
          </ContextMenuItem>
          <ContextMenuItem
            value="duplicate document"
            onClick={() => setLastAction("Duplicated document")}
          >
            Duplicate
          </ContextMenuItem>
          <ContextMenuItem
            value="download export"
            onClick={() => setLastAction("Download selected")}
          >
            Download
          </ContextMenuItem>
          <ContextMenuItem
            value="archive document"
            onClick={() => setLastAction("Archive selected")}
          >
            Archive
          </ContextMenuItem>
          <ContextMenuEmptyState>No matching commands.</ContextMenuEmptyState>
        </ContextMenuContent>
      </ContextMenu>
      <p className="text-center text-xs text-muted-foreground" aria-live="polite">
        {lastAction}
      </p>
    </div>
  )
}

```

## API reference

`ContextMenu` accepts the matching Base UI Context Menu root props plus optional search props: `autocomplete`, `autocompleteMode`, `inputValue`, `defaultInputValue`, and `onInputValueChange`. `ContextMenuContent` accepts popup props, positioning offsets, `searchLabel`, and `searchPlaceholder`.

Items accept Base UI item props plus `value`, `leadingIcon`, `trailingIcon`, `inset`, and `variant`. The `value` supplies searchable text when it should differ from the visible item label. See the [Base UI Context Menu API](https://base-ui.com/react/components/context-menu#api-reference) for the underlying open-state, event, positioning, and interaction details.
