Skip to content
Folio

Overlays

ContextMenu

The menu a right-click opens.

Usage

When to reach for it

Never as the only way to reach an action. Touch users, trackpad users and keyboard users may have no way to open it.
TSX
import { ContextMenu } from '@misoto22/design'

Notes

Radix ContextMenu root and trigger, as typed passthroughs.

Anatomy

ContextMenu anatomy
ElementDescription
TriggerrequiredThe region a secondary click opens the menu over. Radix wraps children in its own inline span unless you pass asChild, and sets -webkit-touch-callout: none on it so the OS text callout does not fire first.
PanelrequiredThe portalled menu, placed at the POINTER rather than against the trigger — there is no side or align to set here, only the 8px collision padding that keeps it inside the viewport or the OverlayContainer frame.
ItemA row, taking the same icon, destructive and disabled props as DropdownMenuItem — icon in either spelling — and highlighting on the same data-highlighted.
LabelA mono eyebrow, on its own, and visual only. ContextMenuGroup is the one that heads rows: it renders role="group" and points its aria-labelledby at this.
GroupA named section: role="group" around the rows, with label rendered inside it as the Label and named through aria-labelledby.
SeparatorA hairline between groups, as a real role="separator".

Best practices

Do

  • Pass asChild and hand it the element itself: without it Radix inserts a span between you and your child, and that span becomes the flex or grid item while your card is laid out inside it as inline content.
  • Build the same array of actions into a DropdownMenu behind an overflow button — the two take identical icon, destructive and disabled props, so one list feeds both and the right-click becomes the shortcut rather than the only door.
  • Wrap the subtree in OverlayContainer when the right-clickable region lives in a scrolling or bounded frame: this is the one panel whose position the reader personally chose, and a flip against a viewport edge they cannot see lands it somewhere they did not point.

Don’t

  • Do not read “no touch support” as the whole story: Radix opens the menu on a 700ms long press for touch and pen, but cancels the moment the pointer moves — so on a scrollable list the long press and the scroll gesture compete and the scroll usually wins.
  • A bare ContextMenuLabel over rows is a picture of a heading: Radix’s MenuLabel is a plain div with no role and nothing tying it to what follows, so the sections a sighted reader sees arrive as one undivided list. ContextMenuGroup renders both halves and wires them together.
  • Radix defaults modal to true here too, so the page behind is scroll-locked and pointer-inert while the menu is open — a right-click menu over a long list stops the list moving under it, which is right for a short action list and wrong for anything the reader must scroll to answer.

Examples

on a region

The menu a secondary click opens, placed at the POINTER rather than against the trigger — there is no side or align to set, only the collision padding that keeps it inside the frame. Never make it the only way to reach an action: Shift+F10 opens it from the keyboard where the platform supports it, and Radix opens it on a 700ms long press for touch and pen but cancels the moment the pointer moves, so on a scrollable list the scroll gesture usually wins. The label is a plain div with no role, like DropdownMenu's — a visual heading, and nothing a screen reader hears on its way through the rows.

Right-click anywhere in this box

two doors

One array of actions, two doors into it. ContextMenuItem and DropdownMenuItem take identical icon, destructive and disabled props, so the same list feeds both and the right-click becomes the shortcut rather than the only way in — which matters because a touch user, a trackpad user and a keyboard user may have no way to open a context menu at all. Write the list once: two hand-kept copies is how the overflow button ends up missing the action somebody added to the right-click.

kyoto-february.tif

a card trigger

asChild hands Radix the Card itself. Without it Radix inserts a span between the trigger and the child, and that span becomes the grid item while the card is laid out inside it as inline content — a layout that looks broken for a reason nothing on the page points at. The menu is modal, so the page behind is scroll-locked and pointer-inert while it is open: right for a short list of actions on one object, wrong for anything the reader has to scroll the page to answer.

api.misoto22.com

live
Deployed from main four minutes ago. Right-click the card for its actions.

Parts

Composed at the call site rather than configured through props, so a layout this component did not anticipate is still expressible.

ContextMenuContent

The menu a right-click opens.

Never the only way to reach an action. A context menu is opened by a secondary pointer button or a keyboard's own menu key, and a meaningful share of readers have neither — a touch user, someone on a trackpad they have not configured, anyone driving the page by keyboard alone. Whatever is in here belongs somewhere reachable too: a row's overflow button, a toolbar.

Takes no props of its own.

ContextMenuItem

A row. Highlight follows data-highlighted, which covers hover and keyboard.

ContextMenuItem props
PropTypeDefaultDescription
destructivebooleanfalsePaints the row as destructive. Use for delete, revoke, disconnect.
iconRemixiconComponentType | ReactNodeOptional leading icon. Either spelling — `icon={Copy}` passes the component and this sizes it, `icon={<Copy size={16} />}` passes the element and this places it.

Also accepts everything in ComponentProps<typeof ContextMenuPrimitive.Item>. Those are forwarded to the underlying element and are not listed row by row.

ContextMenuSeparator

Hairline divider between groups.

Takes no props of its own.

ContextMenuLabel

Mono eyebrow heading, on its own.

Visual only: Radix renders it as a bare <div> with no role. Reach for ContextMenuGroup when the eyebrow is a HEADING over rows; this is right for a line that heads nothing.

Takes no props of its own.

ContextMenuGroup

A named section of a menu.

The eyebrow alone was a picture of a heading: Radix's MenuLabel carries no role and no aria-labelledby wiring, so a sighted reader saw sections and a screen-reader user got one undifferentiated list. This renders the group, renders the label inside it, and points the one at the other.

ContextMenuGroup props
PropTypeDefaultDescription
labelReactNodeThe eyebrow over the rows, and the group's accessible name.

Also accepts everything in ComponentProps<typeof ContextMenuPrimitive.Group>. Those are forwarded to the underlying element and are not listed row by row.

Re-exports

ContextMenu = ContextMenuPrimitive.Root

Radix ContextMenu root and trigger, as typed passthroughs.

ContextMenuTrigger = ContextMenuPrimitive.Trigger

Keyboard

ContextMenu keyboard interactions
KeyDoes
ShiftF10Opens the menu from the keyboard, where the platform supports it.
Moves between items.
EscapeCloses it.