Components

Context Menu

Opens a menu at the exact spot you right-click, providing quick access to relevant options.

beta

Basic

This component excels in desktop applications but has significant limitations. Its lack of accessibility and mobile compatibility are notable drawbacks. React Aria hasn't released an official version of this component yet, possibly due to these issues.

Unless it's absolutely necessary for your project, I'd advise against using it. For most scenarios requiring accessible, rich interfaces, consider alternatives like Menus or Popovers instead. These components typically offer better cross-platform support and accessibility features.

The initial release will be an alpha version due to ongoing issues with cursor positioning accuracy. If you're able to contribute, I'd appreciate your help in submitting a pull request to address this component's limitations.

Installation

If you hit any issues, make sure you check out the installation guide here for more information.

npx @intentui/cli@latest add context-menu

Composed Components

When you install this component via the CLI, it automatically loads all composed components, so you don’t need to add them individually.

The Context Menu comes packed with several components to enhance functionality and provide a seamless experience.

A collapsible menu that reveals actions or navigation options when triggered, distinct from a select as it is not used for form input.

Manual Installation

Make sure you also install the composed components and the required packages for the component to function properly.

npm i react-aria-components @intentui/icons

You can copy the code below and paste it into your component folder.

context-menu.tsx
"use client"

import { createContext, use, useRef, useState } from "react"

import { twMerge } from "tailwind-merge"
import type { MenuContentProps } from "./menu"
import { Menu } from "./menu"

interface ContextMenuTriggerContextType {
  buttonRef: React.RefObject<HTMLButtonElement | null>
  contextMenuOffset: { offset: number; crossOffset: number } | null
  setContextMenuOffset: React.Dispatch<
    React.SetStateAction<{ offset: number; crossOffset: number } | null>
  >
}

const ContextMenuTriggerContext = createContext<ContextMenuTriggerContextType | undefined>(
  undefined,
)

const useContextMenuTrigger = () => {
  const context = use(ContextMenuTriggerContext)
  if (!context) {
    throw new Error("useContextMenuTrigger must be used within a ContextMenuTrigger")
  }
  return context
}

interface ContextMenuProps {
  children: React.ReactNode
}

const ContextMenu = ({ children }: ContextMenuProps) => {
  const [contextMenuOffset, setContextMenuOffset] = useState<{
    offset: number
    crossOffset: number
  } | null>(null)
  const buttonRef = useRef<HTMLButtonElement>(null)
  return (
    <ContextMenuTriggerContext.Provider
      value={{ buttonRef, contextMenuOffset, setContextMenuOffset }}
    >
      {children}
    </ContextMenuTriggerContext.Provider>
  )
}

type ContextMenuTriggerProps = React.ButtonHTMLAttributes<HTMLButtonElement>

const ContextMenuTrigger = ({ className, ...props }: ContextMenuTriggerProps) => {
  const { buttonRef, setContextMenuOffset } = useContextMenuTrigger()

  const onContextMenu = (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault()
    const rect = e.currentTarget.getBoundingClientRect()
    setContextMenuOffset({
      offset: e.clientY - rect.bottom,
      crossOffset: e.clientX - rect.left,
    })
  }
  return (
    <button
      className={twMerge(
        "cursor-default focus:outline-hidden disabled:opacity-60 disabled:forced-colors:disabled:text-[GrayText]",
        className,
      )}
      ref={buttonRef}
      aria-haspopup="menu"
      onContextMenu={onContextMenu}
      {...props}
    />
  )
}

type ContextMenuContentProps<T> = Omit<
  MenuContentProps<T>,
  "showArrow" | "isOpen" | "onOpenChange" | "triggerRef" | "placement" | "shouldFlip"
>

const ContextMenuContent = <T extends object>(props: ContextMenuContentProps<T>) => {
  const { contextMenuOffset, setContextMenuOffset, buttonRef } = useContextMenuTrigger()
  return contextMenuOffset ? (
    <Menu.Content
      isOpen={!!contextMenuOffset}
      onOpenChange={() => setContextMenuOffset(null)}
      triggerRef={buttonRef}
      shouldFlip={false}
      placement="bottom left"
      offset={contextMenuOffset?.offset}
      crossOffset={contextMenuOffset?.crossOffset}
      onClose={() => setContextMenuOffset(null)}
      {...props}
    />
  ) : null
}

const ContextMenuItem = Menu.Item
const ContextMenuSeparator = Menu.Separator
const ContextMenuItemDetails = Menu.ItemDetails
const ContextMenuSection = Menu.Section
const ContextMenuHeader = Menu.Header
const ContextMenuKeyboard = Menu.Keyboard
const ContextMenuLabel = Menu.Label

ContextMenu.Trigger = ContextMenuTrigger
ContextMenu.Content = ContextMenuContent
ContextMenu.Item = ContextMenuItem
ContextMenu.Label = ContextMenuLabel
ContextMenu.Separator = ContextMenuSeparator
ContextMenu.ItemDetails = ContextMenuItemDetails
ContextMenu.Section = ContextMenuSection
ContextMenu.Header = ContextMenuHeader
ContextMenu.Keyboard = ContextMenuKeyboard

export type { ContextMenuProps }
export { ContextMenu }

Item Details

You can add details to menu items by using the ContextMenuItemDetails component.

Danger

Designate a menu item as dangerous.

With Icon

Enhance context menu item by adding icons.

Separator

Separate context menu items with a separator.

Context Sub Menu

Sorry, this component doesn't exist yet. It's on our roadmap.

Disabled

Disable specific menu items.

Also, you can disable items directly in MenuContent by using the disabledKeys prop.

<ContextMenu.Content disabledKeys={['gsu']} />
Get premium Blocks
Create stunning, professional-grade layouts that not only save time but also elevate the quality of your projects.