Overlays
ContextMenu
The menu a right-click opens.
Usage
When to reach for it
import { ContextMenu } from '@misoto22/design'Notes
Radix ContextMenu root and trigger, as typed passthroughs.
Anatomy
| Element | Description |
|---|---|
| Triggerrequired | The 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. |
| Panelrequired | The 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. |
| Item | A row, taking the same icon, destructive and disabled props as DropdownMenuItem — icon in either spelling — and highlighting on the same data-highlighted. |
| Label | A 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. |
| Group | A named section: role="group" around the rows, with label rendered inside it as the Label and named through aria-labelledby. |
| Separator | A 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.
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
liveParts
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.
| Prop | Type | Default | Description |
|---|---|---|---|
| destructive | boolean | false | Paints the row as destructive. Use for delete, revoke, disconnect. |
| icon | RemixiconComponentType | ReactNode | Optional 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.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | ReactNode | The 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.RootRadix ContextMenu root and trigger, as typed passthroughs.
ContextMenuTrigger = ContextMenuPrimitive.TriggerKeyboard
| Key | Does |
|---|---|
| ShiftF10 | Opens the menu from the keyboard, where the platform supports it. |
| ↑↓ | Moves between items. |
| Escape | Closes it. |