# Context Menu

Displays a menu of actions, opened by a right click or a long press.

```tsx
import * as stylex from "@stylexjs/stylex"
import {
  ContextMenu,
  ContextMenuCheckboxItem,
  ContextMenuContent,
  ContextMenuGroup,
  ContextMenuItem,
  ContextMenuLabel,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { leading, radius, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  dropzone: {
    display: "flex",
    aspectRatio: "16 / 9",
    width: "100%",
    maxWidth: "20rem",
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.xl,
    borderWidth: 1,
    borderStyle: "dashed",
    fontSize: text.sm,
    lineHeight: leading.sm,
  },
  fine: { display: { default: "none", "@media (pointer: fine)": "inline" } },
  coarse: { display: { default: "none", "@media (pointer: coarse)": "inline" } },
  menu: { minWidth: "12rem" },
})

export default function ContextMenuDemo() {
  return (
    <ContextMenu>
      <ContextMenuTrigger xstyle={styles.dropzone}>
        <span {...stylex.props(styles.fine)}>Right click here</span>
        <span {...stylex.props(styles.coarse)}>Long press here</span>
      </ContextMenuTrigger>
      <ContextMenuContent xstyle={styles.menu}>
        <ContextMenuGroup>
          <ContextMenuItem>
            Back
            <ContextMenuShortcut>⌘[</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuItem disabled>
            Forward
            <ContextMenuShortcut>⌘]</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuItem>
            Reload
            <ContextMenuShortcut>⌘R</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuSub>
            <ContextMenuSubTrigger>More Tools</ContextMenuSubTrigger>
            <ContextMenuSubContent>
              <ContextMenuItem>Save Page...</ContextMenuItem>
              <ContextMenuItem>Create Shortcut...</ContextMenuItem>
              <ContextMenuItem>Name Window...</ContextMenuItem>
              <ContextMenuSeparator />
              <ContextMenuItem>Developer Tools</ContextMenuItem>
              <ContextMenuSeparator />
              <ContextMenuItem variant="destructive">Delete</ContextMenuItem>
            </ContextMenuSubContent>
          </ContextMenuSub>
        </ContextMenuGroup>
        <ContextMenuSeparator />
        <ContextMenuGroup>
          <ContextMenuCheckboxItem defaultChecked>Show Bookmarks</ContextMenuCheckboxItem>
          <ContextMenuCheckboxItem>Show Full URLs</ContextMenuCheckboxItem>
        </ContextMenuGroup>
        <ContextMenuSeparator />
        <ContextMenuGroup>
          <ContextMenuLabel>People</ContextMenuLabel>
          <ContextMenuRadioGroup defaultValue="pedro">
            <ContextMenuRadioItem value="pedro">Pedro Duarte</ContextMenuRadioItem>
            <ContextMenuRadioItem value="colm">Colm Tuite</ContextMenuRadioItem>
          </ContextMenuRadioGroup>
        </ContextMenuGroup>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx lucide-react
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `states.ts` and `state-styles.ts`
   from the same page. The popup surface is shared, and the items read their
   highlighted and disabled styles from Base UI's state. Include `interop.css`
   for item icon sizing.

3. Copy and paste the following code into your project.

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

   import { useRender } from "@base-ui/react/use-render"
   import type { CSSProperties } from "react"
   import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"
   import { Check, ChevronRight } from "lucide-react"

   import { stateStyles } from "@/lib/state-styles"
   import { states } from "@/theme/states"
   import {
     color,
     duration,
     ease,
     layer,
     leading,
     radius,
     shadow,
     space,
     text,
     tracking,
   } from "@/theme/tokens.stylex"

   const styles = stylex.create({
     hidden: { opacity: 0 },
     trigger: { userSelect: "none" },
     content: {
       zIndex: layer.popup,
       maxHeight: "var(--available-height)",
       minWidth: "9rem",
       overflowX: "hidden",
       overflowY: "auto",
       "--carbon-popup-radius": radius.lg,
       borderRadius: "var(--carbon-popup-radius)",
       padding: space.s1,
       transformOrigin: "var(--transform-origin)",
       transitionProperty: "opacity, transform",
       transitionDuration: {
         default: duration.fade,
         "@media (prefers-reduced-motion: reduce)": "0s",
       },
       transitionTimingFunction: ease.standard,
     },
     subContent: {
       width: "auto",
       minWidth: "8rem",
       boxShadow: shadow.popupLifted,
     },
     label: {
       paddingInline: space.s1_5,
       paddingBlock: space.s1,
       fontSize: text.xs,
       lineHeight: leading.xs,
       fontWeight: 500,
       color: color.mutedForeground,
     },
     item: {
       position: "relative",
       display: "flex",
       alignItems: "center",
       gap: space.s1_5,
       cursor: "default",
       userSelect: "none",
       paddingInline: space.s1_5,
       paddingBlock: space.s1,
       fontSize: text.sm,
       lineHeight: leading.sm,
       outlineStyle: "none",
       "--carbon-icon-size": space.s4,
     },
     inset: { paddingInlineStart: space.s7 },
     destructive: { color: color.destructive },
     indicatorItem: { paddingInlineEnd: space.s8 },
     indicator: {
       pointerEvents: "none",
       position: "absolute",
       insetInlineEnd: space.s2,
       display: "flex",
       alignItems: "center",
       justifyContent: "center",
     },
     chevron: { marginInlineStart: "auto" },
     separator: {
       marginInline: `calc(-1 * ${space.s1})`,
       marginBlock: space.s1,
       height: 1,
       backgroundColor: color.border,
     },
     shortcut: {
       marginInlineStart: "auto",
       fontSize: text.xs,
       lineHeight: leading.xs,
       letterSpacing: tracking.widest,
       color: color.mutedForeground,
     },
   })

   type ItemVariant = "default" | "destructive"

   const slide = stylex.create({
     top: { transform: `scale(0.95) translateY(${space.s2})` },
     bottom: { transform: `scale(0.95) translateY(calc(-1 * ${space.s2}))` },
     left: { transform: `scale(0.95) translateX(${space.s2})` },
     right: { transform: `scale(0.95) translateX(calc(-1 * ${space.s2}))` },
     "inline-start": { transform: `scale(0.95) translateX(${space.s2})` },
     "inline-end": { transform: `scale(0.95) translateX(calc(-1 * ${space.s2}))` },
   })

   type Overrides = { xstyle?: StyleXStyles; className?: string }

   type StateOverrides = Overrides & { style?: CSSProperties }

   function ContextMenu(props: ContextMenuPrimitive.Root.Props) {
     return <ContextMenuPrimitive.Root {...props} />
   }

   function ContextMenuPortal(props: ContextMenuPrimitive.Portal.Props) {
     return <ContextMenuPrimitive.Portal {...props} />
   }

   type ContextMenuTriggerProps = Omit<ContextMenuPrimitive.Trigger.Props, "className"> & Overrides

   function ContextMenuTrigger({ xstyle, className, ...props }: ContextMenuTriggerProps) {
     const sx = stylex.props(styles.trigger, xstyle)
     return (
       <ContextMenuPrimitive.Trigger
         data-slot="context-menu-trigger"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   type ContextMenuContentProps = Omit<ContextMenuPrimitive.Popup.Props, "className" | "style"> &
     Pick<ContextMenuPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset"> &
     StateOverrides

   function ContextMenuContent({
     xstyle,
     style,
     className,
     side = "right",
     sideOffset = 0,
     align = "start",
     alignOffset = 4,
     ...props
   }: ContextMenuContentProps) {
     return (
       <ContextMenuPrimitive.Portal>
         <ContextMenuPrimitive.Positioner
           align={align}
           alignOffset={alignOffset}
           side={side}
           sideOffset={sideOffset}
           {...stylex.props(states.positioner)}
         >
           <ContextMenuPrimitive.Popup
             data-slot="context-menu-content"
             {...props}
             {...stateStyles(
               (state) => [
                 states.popupSurface,
                 styles.content,
                 (state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
                   styles.hidden,
                 (state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
                   slide[state.side],
                 xstyle,
               ],
               { className, style },
             )}
           />
         </ContextMenuPrimitive.Positioner>
       </ContextMenuPrimitive.Portal>
     )
   }

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

   type ContextMenuLabelProps = Omit<ContextMenuPrimitive.GroupLabel.Props, "className"> &
     Overrides & { inset?: boolean }

   function ContextMenuLabel({ xstyle, className, inset, ...props }: ContextMenuLabelProps) {
     const sx = stylex.props(styles.label, inset && styles.inset, xstyle)
     return (
       <ContextMenuPrimitive.GroupLabel
         data-slot="context-menu-label"
         data-inset={inset || undefined}
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   type ContextMenuItemProps = Omit<ContextMenuPrimitive.Item.Props, "className" | "style"> &
     StateOverrides & { inset?: boolean; variant?: ItemVariant }

   function ContextMenuItem({
     xstyle,
     style,
     className,
     inset,
     variant = "default",
     ...props
   }: ContextMenuItemProps) {
     const destructive = variant === "destructive"
     return (
       <ContextMenuPrimitive.Item
         data-slot="context-menu-item"
         data-inset={inset || undefined}
         data-variant={variant}
         {...props}
         {...stateStyles(
           (state) => [
             styles.item,
             states.popupItem,
             inset && styles.inset,
             destructive && styles.destructive,
             state.highlighted &&
               (destructive ? states.itemHighlightDestructive : states.itemHighlight),
             state.disabled && states.disabled,
             xstyle,
           ],
           { className, style, hook: "carbon-icons" },
         )}
       />
     )
   }

   type ContextMenuCheckboxItemProps = Omit<
     ContextMenuPrimitive.CheckboxItem.Props,
     "className" | "style"
   > &
     StateOverrides & { inset?: boolean }

   function ContextMenuCheckboxItem({
     xstyle,
     style,
     className,
     inset,
     children,
     ...props
   }: ContextMenuCheckboxItemProps) {
     return (
       <ContextMenuPrimitive.CheckboxItem
         data-slot="context-menu-checkbox-item"
         data-inset={inset || undefined}
         {...props}
         {...stateStyles(
           (state) => [
             styles.item,
             states.popupItem,
             styles.indicatorItem,
             inset && styles.inset,
             state.highlighted && states.itemHighlight,
             state.disabled && states.disabled,
             xstyle,
           ],
           { className, style, hook: "carbon-icons" },
         )}
       >
         <span data-slot="context-menu-checkbox-item-indicator" {...stylex.props(styles.indicator)}>
           <ContextMenuPrimitive.CheckboxItemIndicator>
             <Check />
           </ContextMenuPrimitive.CheckboxItemIndicator>
         </span>
         {children}
       </ContextMenuPrimitive.CheckboxItem>
     )
   }

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

   type ContextMenuRadioItemProps = Omit<ContextMenuPrimitive.RadioItem.Props, "className" | "style"> &
     StateOverrides & { inset?: boolean }

   function ContextMenuRadioItem({
     xstyle,
     style,
     className,
     inset,
     children,
     ...props
   }: ContextMenuRadioItemProps) {
     return (
       <ContextMenuPrimitive.RadioItem
         data-slot="context-menu-radio-item"
         data-inset={inset || undefined}
         {...props}
         {...stateStyles(
           (state) => [
             styles.item,
             states.popupItem,
             styles.indicatorItem,
             inset && styles.inset,
             state.highlighted && states.itemHighlight,
             state.disabled && states.disabled,
             xstyle,
           ],
           { className, style, hook: "carbon-icons" },
         )}
       >
         <span data-slot="context-menu-radio-item-indicator" {...stylex.props(styles.indicator)}>
           <ContextMenuPrimitive.RadioItemIndicator>
             <Check />
           </ContextMenuPrimitive.RadioItemIndicator>
         </span>
         {children}
       </ContextMenuPrimitive.RadioItem>
     )
   }

   type ContextMenuSeparatorProps = Omit<ContextMenuPrimitive.Separator.Props, "className"> & Overrides

   function ContextMenuSeparator({ xstyle, className, ...props }: ContextMenuSeparatorProps) {
     const sx = stylex.props(styles.separator, xstyle)
     return (
       <ContextMenuPrimitive.Separator
         data-slot="context-menu-separator"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   type ContextMenuShortcutProps = useRender.ComponentProps<"span"> & Overrides

   function ContextMenuShortcut({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: ContextMenuShortcutProps) {
     const sx = stylex.props(styles.shortcut, xstyle)
     return useRender({
       defaultTagName: "span",
       render,
       ref,
       props: {
         "data-slot": "context-menu-shortcut",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

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

   type ContextMenuSubTriggerProps = Omit<
     ContextMenuPrimitive.SubmenuTrigger.Props,
     "className" | "style"
   > &
     StateOverrides & { inset?: boolean }

   function ContextMenuSubTrigger({
     xstyle,
     style,
     className,
     inset,
     children,
     ...props
   }: ContextMenuSubTriggerProps) {
     return (
       <ContextMenuPrimitive.SubmenuTrigger
         data-slot="context-menu-sub-trigger"
         data-inset={inset || undefined}
         {...props}
         {...stateStyles(
           (state) => [
             styles.item,
             states.popupItem,
             inset && styles.inset,
             (state.highlighted || state.open) && states.itemHighlight,
             state.disabled && states.disabled,
             xstyle,
           ],
           { className, style, hook: "carbon-icons" },
         )}
       >
         {children}
         <ChevronRight {...stylex.props(styles.chevron)} />
       </ContextMenuPrimitive.SubmenuTrigger>
     )
   }

   function ContextMenuSubContent({
     xstyle,
     side = "right",
     sideOffset = 0,
     align = "start",
     alignOffset = -3,
     ...props
   }: ContextMenuContentProps) {
     return (
       <ContextMenuContent
         data-slot="context-menu-sub-content"
         side={side}
         sideOffset={sideOffset}
         align={align}
         alignOffset={alignOffset}
         xstyle={[styles.subContent, xstyle]}
         {...props}
       />
     )
   }

   export {
     ContextMenu,
     ContextMenuCheckboxItem,
     ContextMenuContent,
     ContextMenuGroup,
     ContextMenuItem,
     ContextMenuLabel,
     ContextMenuPortal,
     ContextMenuRadioGroup,
     ContextMenuRadioItem,
     ContextMenuSeparator,
     ContextMenuShortcut,
     ContextMenuSub,
     ContextMenuSubContent,
     ContextMenuSubTrigger,
     ContextMenuTrigger,
   }
   ```

4. Update the import paths to match your project setup.

## Usage

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

```tsx
<ContextMenu>
  <ContextMenuTrigger>Right click here</ContextMenuTrigger>
  <ContextMenuContent>
    <ContextMenuItem>Rename</ContextMenuItem>
    <ContextMenuItem>Duplicate</ContextMenuItem>
    <ContextMenuSeparator />
    <ContextMenuItem variant="destructive">Delete</ContextMenuItem>
  </ContextMenuContent>
</ContextMenu>
```

`ContextMenuTrigger` renders a bare `<div>` with no size and no appearance. Give
it both, or pass the element you already have through `render`. A right click or
a half-second long press opens the menu.

> **Note**
>
> The menu anchors to the point that opened it, not to the trigger box. `side` and
> `align` are measured from the cursor, so a trigger that fills the viewport still
> gets a menu beside the pointer.

## Basic

The content holds items directly. Nothing has to be grouped.

```tsx
import * as stylex from "@stylexjs/stylex"
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { leading, radius, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  dropzone: {
    display: "flex",
    aspectRatio: "16 / 9",
    width: "100%",
    maxWidth: "20rem",
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.xl,
    borderWidth: 1,
    borderStyle: "dashed",
    fontSize: text.sm,
    lineHeight: leading.sm,
  },
  fine: { display: { default: "none", "@media (pointer: fine)": "inline" } },
  coarse: { display: { default: "none", "@media (pointer: coarse)": "inline" } },
})

export default function ContextMenuBasic() {
  return (
    <ContextMenu>
      <ContextMenuTrigger xstyle={styles.dropzone}>
        <span {...stylex.props(styles.fine)}>Right click here</span>
        <span {...stylex.props(styles.coarse)}>Long press here</span>
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem>Back</ContextMenuItem>
        <ContextMenuItem disabled>Forward</ContextMenuItem>
        <ContextMenuItem>Reload</ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

## Submenu

Nest secondary actions in a `ContextMenuSub`. A submenu anchors to its trigger
row rather than to the cursor.

```tsx
import * as stylex from "@stylexjs/stylex"
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { leading, radius, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  dropzone: {
    display: "flex",
    aspectRatio: "16 / 9",
    width: "100%",
    maxWidth: "20rem",
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.xl,
    borderWidth: 1,
    borderStyle: "dashed",
    fontSize: text.sm,
    lineHeight: leading.sm,
  },
  fine: { display: { default: "none", "@media (pointer: fine)": "inline" } },
  coarse: { display: { default: "none", "@media (pointer: coarse)": "inline" } },
})

export default function ContextMenuSubmenu() {
  return (
    <ContextMenu>
      <ContextMenuTrigger xstyle={styles.dropzone}>
        <span {...stylex.props(styles.fine)}>Right click here</span>
        <span {...stylex.props(styles.coarse)}>Long press here</span>
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem>
          Copy
          <ContextMenuShortcut>⌘C</ContextMenuShortcut>
        </ContextMenuItem>
        <ContextMenuItem>
          Cut
          <ContextMenuShortcut>⌘X</ContextMenuShortcut>
        </ContextMenuItem>
        <ContextMenuSub>
          <ContextMenuSubTrigger>More Tools</ContextMenuSubTrigger>
          <ContextMenuSubContent>
            <ContextMenuItem>Save Page...</ContextMenuItem>
            <ContextMenuItem>Create Shortcut...</ContextMenuItem>
            <ContextMenuSub>
              <ContextMenuSubTrigger>Share</ContextMenuSubTrigger>
              <ContextMenuSubContent>
                <ContextMenuItem>Email Link</ContextMenuItem>
                <ContextMenuItem>Copy Link</ContextMenuItem>
              </ContextMenuSubContent>
            </ContextMenuSub>
            <ContextMenuSeparator />
            <ContextMenuItem>Developer Tools</ContextMenuItem>
          </ContextMenuSubContent>
        </ContextMenuSub>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

## Shortcuts

Use `ContextMenuShortcut` to show a key combination at the trailing edge of an
item. It displays the key only. Wire the binding yourself.

```tsx
import * as stylex from "@stylexjs/stylex"
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuGroup,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { leading, radius, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  dropzone: {
    display: "flex",
    aspectRatio: "16 / 9",
    width: "100%",
    maxWidth: "20rem",
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.xl,
    borderWidth: 1,
    borderStyle: "dashed",
    fontSize: text.sm,
    lineHeight: leading.sm,
  },
  fine: { display: { default: "none", "@media (pointer: fine)": "inline" } },
  coarse: { display: { default: "none", "@media (pointer: coarse)": "inline" } },
})

export default function ContextMenuShortcuts() {
  return (
    <ContextMenu>
      <ContextMenuTrigger xstyle={styles.dropzone}>
        <span {...stylex.props(styles.fine)}>Right click here</span>
        <span {...stylex.props(styles.coarse)}>Long press here</span>
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuGroup>
          <ContextMenuItem>
            Back
            <ContextMenuShortcut>⌘[</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuItem disabled>
            Forward
            <ContextMenuShortcut>⌘]</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuItem>
            Reload
            <ContextMenuShortcut>⌘R</ContextMenuShortcut>
          </ContextMenuItem>
        </ContextMenuGroup>
        <ContextMenuSeparator />
        <ContextMenuGroup>
          <ContextMenuItem>
            Save
            <ContextMenuShortcut>⌘S</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuItem>
            Save As...
            <ContextMenuShortcut>⇧⌘S</ContextMenuShortcut>
          </ContextMenuItem>
        </ContextMenuGroup>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

## Groups

Wrap related items in a `ContextMenuGroup`, name it with `ContextMenuLabel` and
divide groups with `ContextMenuSeparator`. A screen reader announces the label
as the name of its group.

```tsx
import * as stylex from "@stylexjs/stylex"
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuGroup,
  ContextMenuItem,
  ContextMenuLabel,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { leading, radius, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  dropzone: {
    display: "flex",
    aspectRatio: "16 / 9",
    width: "100%",
    maxWidth: "20rem",
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.xl,
    borderWidth: 1,
    borderStyle: "dashed",
    fontSize: text.sm,
    lineHeight: leading.sm,
  },
  fine: { display: { default: "none", "@media (pointer: fine)": "inline" } },
  coarse: { display: { default: "none", "@media (pointer: coarse)": "inline" } },
})

export default function ContextMenuGroups() {
  return (
    <ContextMenu>
      <ContextMenuTrigger xstyle={styles.dropzone}>
        <span {...stylex.props(styles.fine)}>Right click here</span>
        <span {...stylex.props(styles.coarse)}>Long press here</span>
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuGroup>
          <ContextMenuLabel>File</ContextMenuLabel>
          <ContextMenuItem>
            New File
            <ContextMenuShortcut>⌘N</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuItem>
            Open File
            <ContextMenuShortcut>⌘O</ContextMenuShortcut>
          </ContextMenuItem>
        </ContextMenuGroup>
        <ContextMenuSeparator />
        <ContextMenuGroup>
          <ContextMenuLabel>Edit</ContextMenuLabel>
          <ContextMenuItem>
            Undo
            <ContextMenuShortcut>⌘Z</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuItem>
            Redo
            <ContextMenuShortcut>⇧⌘Z</ContextMenuShortcut>
          </ContextMenuItem>
        </ContextMenuGroup>
        <ContextMenuSeparator />
        <ContextMenuGroup>
          <ContextMenuItem variant="destructive">
            Delete
            <ContextMenuShortcut>⌫</ContextMenuShortcut>
          </ContextMenuItem>
        </ContextMenuGroup>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

## Icons

Place an icon before the label. The item sizes it.

```tsx
import * as stylex from "@stylexjs/stylex"
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { leading, radius, text } from "@/theme/tokens.stylex"
import { ClipboardPaste, Copy, Scissors, Trash } from "lucide-react"

const styles = stylex.create({
  dropzone: {
    display: "flex",
    aspectRatio: "16 / 9",
    width: "100%",
    maxWidth: "20rem",
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.xl,
    borderWidth: 1,
    borderStyle: "dashed",
    fontSize: text.sm,
    lineHeight: leading.sm,
  },
  fine: { display: { default: "none", "@media (pointer: fine)": "inline" } },
  coarse: { display: { default: "none", "@media (pointer: coarse)": "inline" } },
})

export default function ContextMenuIcons() {
  return (
    <ContextMenu>
      <ContextMenuTrigger xstyle={styles.dropzone}>
        <span {...stylex.props(styles.fine)}>Right click here</span>
        <span {...stylex.props(styles.coarse)}>Long press here</span>
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem>
          <Copy />
          Copy
        </ContextMenuItem>
        <ContextMenuItem>
          <Scissors />
          Cut
        </ContextMenuItem>
        <ContextMenuItem>
          <ClipboardPaste />
          Paste
        </ContextMenuItem>
        <ContextMenuSeparator />
        <ContextMenuItem variant="destructive">
          <Trash />
          Delete
        </ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

## Checkboxes

Use `ContextMenuCheckboxItem` for items that toggle. Pass `checked` and
`onCheckedChange` to control it, or `defaultChecked` to leave it uncontrolled.

```tsx
import * as stylex from "@stylexjs/stylex"
import {
  ContextMenu,
  ContextMenuCheckboxItem,
  ContextMenuContent,
  ContextMenuGroup,
  ContextMenuLabel,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { leading, radius, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  dropzone: {
    display: "flex",
    aspectRatio: "16 / 9",
    width: "100%",
    maxWidth: "20rem",
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.xl,
    borderWidth: 1,
    borderStyle: "dashed",
    fontSize: text.sm,
    lineHeight: leading.sm,
  },
  fine: { display: { default: "none", "@media (pointer: fine)": "inline" } },
  coarse: { display: { default: "none", "@media (pointer: coarse)": "inline" } },
  menu: { minWidth: "12rem" },
})

export default function ContextMenuCheckboxes() {
  return (
    <ContextMenu>
      <ContextMenuTrigger xstyle={styles.dropzone}>
        <span {...stylex.props(styles.fine)}>Right click here</span>
        <span {...stylex.props(styles.coarse)}>Long press here</span>
      </ContextMenuTrigger>
      <ContextMenuContent xstyle={styles.menu}>
        <ContextMenuGroup>
          <ContextMenuLabel>Appearance</ContextMenuLabel>
          <ContextMenuCheckboxItem defaultChecked>Show Bookmarks Bar</ContextMenuCheckboxItem>
          <ContextMenuCheckboxItem>Show Full URLs</ContextMenuCheckboxItem>
          <ContextMenuCheckboxItem disabled>Show Developer Tools</ContextMenuCheckboxItem>
        </ContextMenuGroup>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

## Radio group

Use `ContextMenuRadioGroup` to hold one value across its `ContextMenuRadioItem`s.
Pass `value` and `onValueChange`, or `defaultValue`.

```tsx
"use client"

import { useState } from "react"
import * as stylex from "@stylexjs/stylex"
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuGroup,
  ContextMenuLabel,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  ContextMenuSeparator,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { leading, radius, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  dropzone: {
    display: "flex",
    aspectRatio: "16 / 9",
    width: "100%",
    maxWidth: "20rem",
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.xl,
    borderWidth: 1,
    borderStyle: "dashed",
    fontSize: text.sm,
    lineHeight: leading.sm,
  },
  fine: { display: { default: "none", "@media (pointer: fine)": "inline" } },
  coarse: { display: { default: "none", "@media (pointer: coarse)": "inline" } },
})

export default function ContextMenuRadio() {
  const [person, setPerson] = useState("pedro")
  const [theme, setTheme] = useState("light")

  return (
    <ContextMenu>
      <ContextMenuTrigger xstyle={styles.dropzone}>
        <span {...stylex.props(styles.fine)}>Right click here</span>
        <span {...stylex.props(styles.coarse)}>Long press here</span>
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuGroup>
          <ContextMenuLabel>People</ContextMenuLabel>
          <ContextMenuRadioGroup value={person} onValueChange={setPerson}>
            <ContextMenuRadioItem value="pedro">Pedro Duarte</ContextMenuRadioItem>
            <ContextMenuRadioItem value="colm">Colm Tuite</ContextMenuRadioItem>
          </ContextMenuRadioGroup>
        </ContextMenuGroup>
        <ContextMenuSeparator />
        <ContextMenuGroup>
          <ContextMenuLabel>Theme</ContextMenuLabel>
          <ContextMenuRadioGroup value={theme} onValueChange={setTheme}>
            <ContextMenuRadioItem value="light">Light</ContextMenuRadioItem>
            <ContextMenuRadioItem value="dark">Dark</ContextMenuRadioItem>
            <ContextMenuRadioItem value="system">System</ContextMenuRadioItem>
          </ContextMenuRadioGroup>
        </ContextMenuGroup>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

## Destructive

Set `variant="destructive"` on an item to colour it and its icon.

```tsx
import * as stylex from "@stylexjs/stylex"
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { leading, radius, text } from "@/theme/tokens.stylex"
import { Archive, Pencil, Share, Trash } from "lucide-react"

const styles = stylex.create({
  dropzone: {
    display: "flex",
    aspectRatio: "16 / 9",
    width: "100%",
    maxWidth: "20rem",
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.xl,
    borderWidth: 1,
    borderStyle: "dashed",
    fontSize: text.sm,
    lineHeight: leading.sm,
  },
  fine: { display: { default: "none", "@media (pointer: fine)": "inline" } },
  coarse: { display: { default: "none", "@media (pointer: coarse)": "inline" } },
})

export default function ContextMenuDestructive() {
  return (
    <ContextMenu>
      <ContextMenuTrigger xstyle={styles.dropzone}>
        <span {...stylex.props(styles.fine)}>Right click here</span>
        <span {...stylex.props(styles.coarse)}>Long press here</span>
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem>
          <Pencil />
          Edit
        </ContextMenuItem>
        <ContextMenuItem>
          <Share />
          Share
        </ContextMenuItem>
        <ContextMenuItem>
          <Archive />
          Archive
        </ContextMenuItem>
        <ContextMenuSeparator />
        <ContextMenuItem variant="destructive">
          <Trash />
          Delete
        </ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

## Sides

Set `side` to move the menu around the cursor. Use `"inline-start"` or
`"inline-end"` instead of `"left"` or `"right"` to have the side follow the
writing direction.

```tsx
import * as stylex from "@stylexjs/stylex"
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { leading, radius, space, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  grid: {
    display: "grid",
    width: "100%",
    maxWidth: "24rem",
    gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
    gap: space.s4,
  },
  dropzone: {
    display: "flex",
    aspectRatio: "16 / 9",
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.xl,
    borderWidth: 1,
    borderStyle: "dashed",
    fontSize: text.sm,
    lineHeight: leading.sm,
  },
})

const sides = ["top", "right", "bottom", "left"] as const

export default function ContextMenuSides() {
  return (
    <div {...stylex.props(styles.grid)}>
      {sides.map((side) => (
        <ContextMenu key={side}>
          <ContextMenuTrigger xstyle={styles.dropzone}>{side}</ContextMenuTrigger>
          <ContextMenuContent side={side}>
            <ContextMenuItem>Back</ContextMenuItem>
            <ContextMenuItem>Forward</ContextMenuItem>
            <ContextMenuItem>Reload</ContextMenuItem>
          </ContextMenuContent>
        </ContextMenu>
      ))}
    </div>
  )
}
```

## API reference

Pass `render` to replace a part's element or compose it with another component.

Every part wraps the Base UI `ContextMenu` part of the same name and accepts its
props. See the
[Base UI reference](https://base-ui.com/react/components/context-menu) for the
full list. Each styled part also takes `xstyle` and `className`. The items and
`ContextMenuSubTrigger` take `style` as well, which is real inline CSS applied
over the compiled styles rather than a StyleX override.

`ContextMenu` takes the `Menu` root's props apart from `modal`, `openOnHover`,
`delay` and `closeDelay`, which a context menu has no use for.

### ContextMenuContent

| Prop          | Type                                                                       | Default   |
| ------------- | -------------------------------------------------------------------------- | --------- |
| `side`        | `"top" \| "bottom" \| "left" \| "right" \| "inline-start" \| "inline-end"` | `"right"` |
| `sideOffset`  | `number`                                                                   | `0`       |
| `align`       | `"start" \| "center" \| "end"`                                             | `"start"` |
| `alignOffset` | `number`                                                                   | `4`       |

The defaults place the menu below and to the right of the pointer, the way a
native menu does. The popup is never narrower than `9rem`. Set a wider floor
through `xstyle`.

### ContextMenuSubContent

The same props, with `alignOffset={-3}` as its default, which pulls the submenu
back by the popup padding so its first item lines up with the row that opened
it.

### ContextMenuItem

| Prop       | Type                         | Default     |
| ---------- | ---------------------------- | ----------- |
| `variant`  | `"default" \| "destructive"` | `"default"` |
| `inset`    | `boolean`                    | —           |
| `disabled` | `boolean`                    | —           |

`inset` pads the leading edge by the width of an indicator column, so a plain
item lines up with checkbox and radio items beside it. `ContextMenuLabel`,
`ContextMenuSubTrigger`, `ContextMenuCheckboxItem` and `ContextMenuRadioItem`
take `inset` too.

### ContextMenuCheckboxItem

| Prop              | Type                         | Default |
| ----------------- | ---------------------------- | ------- |
| `checked`         | `boolean`                    | —       |
| `defaultChecked`  | `boolean`                    | —       |
| `onCheckedChange` | `(checked: boolean) => void` | —       |

### ContextMenuRadioGroup

| Prop            | Type                      | Default |
| --------------- | ------------------------- | ------- |
| `value`         | `string`                  | —       |
| `defaultValue`  | `string`                  | —       |
| `onValueChange` | `(value: string) => void` | —       |

### Data attributes

| Attribute                                  | On          | Effect                                                     |
| ------------------------------------------ | ----------- | ---------------------------------------------------------- |
| `data-slot="context-menu-trigger"`         | the trigger | Targets the area from a parent                             |
| `data-slot="context-menu-content"`         | the popup   | Popup surface                                              |
| `data-side`, `data-align`                  | the popup   | Base UI writes them. `data-side` picks the slide direction |
| `data-starting-style`, `data-ending-style` | the popup   | Base UI writes them while the popup animates in or out     |
| `data-slot="context-menu-item"`            | an item     | Targets items from a parent                                |
| `data-variant`                             | an item     | Reflects the `variant` prop                                |
| `data-inset`                               | an item     | Present when `inset` is set                                |
| `data-highlighted`, `data-disabled`        | an item     | Base UI writes them                                        |
