Context Menu
A menu that opens at the pointer, on right click or long press, over a region of the page rather than off a button. It is the same list of commands Menu renders — literally the same components below the trigger — anchored to where the user clicked instead of to a control.
Import
import { ContextMenu } from "@forte-ui/react";Anatomy
Only Root and Trigger are new. Everything below the trigger — the popup,
every row kind, the groups, the separators, the submenus — is the Menu
component: Base UI re-exports the Menu parts under its context-menu entry
point, and so does this library, so ContextMenu.Item and Menu.Item are one
function rather than two that look alike.
Three things follow from that, and they are the ones worth knowing:
- The rows are themed with the
--forte-menu-*knobs, not--forte-context-menu-*. The trigger is the only part with properties of its own. - The parts carry the Menu markers —
data-forte="menu-popup",data-forte="menu-item"— so a consumer who re-skins menu rows from plain CSS re-skins both menus at once, which is almost always what they meant. - A list of rows can be handed to either popup without a wrapper deciding which namespace to render from.
Examples
Pair it with a visible menu
The context menu supplements a <Menu> button on the same card. Actions() is
written once and dropped into both popups — the rows are the same components, so
nothing has to branch on which menu is rendering them.
Station Hofplein
JPG · 2.4 MB
One region per row
A single root around a whole list would open the same menu wherever the pointer
was, and nothing would say which row it meant. Give each row its own
<ContextMenu.Root> instead. render={<li />} keeps the trigger inside the
list's content model — a bare <div> child of a <ul> is invalid HTML.
The ring on the region while its menu is open is doing real work here: the popup opens at the pointer and usually covers the row that produced it, so without the ring there is nothing left on screen tying the two together.
- Q3 report.pdf
- budget.xlsx
- team offsite.md
Checkable rows
<ContextMenu.CheckboxItem> and <ContextMenu.RadioItem> behave exactly as
they do in a dropdown, including the closeOnClick={false} default that lets a
run of toggles be set in one visit.
Submenus
<ContextMenu.SubmenuRoot> is <Menu.SubmenuRoot>, and its popup still needs no
positioning props. The nesting wins over the context menu's anchor-to-the-pointer
placement, so a submenu opens sideways off its row exactly as it would in a
dropdown.
Disabled
disabled on <ContextMenu.Root> turns the region off entirely — and the
browser's own context menu comes back, because the trigger stops calling
preventDefault on the event.
Placement
There is nothing to place. <ContextMenu.Popup> reads the nesting from
<ContextMenu.Root> and anchors itself to the point the pointer was at, with
the offsets that land its corner on the cursor; it flips and shifts to stay
inside the viewport from there. side, align, sideOffset and alignOffset
are all still accepted and still override, but passing side in particular opts
the popup out of pointer anchoring and back onto the anchor rectangle — which
for a context menu is a zero-sized box at the click, so the result is a menu a
few pixels away from where it should be.
Accessibility
| Key | Behaviour |
|---|---|
| Shift + F10 then Menu key | Fires the platform's context-menu event on the focused element. If that element is inside a ContextMenu.Trigger the event bubbles up to it and this menu opens — so the region is keyboard-reachable exactly when it already contains something focusable, and not otherwise. |
| Arrow Down then Arrow Up | Once the menu is open, moves the highlight between rows. |
| Arrow Right | On a submenu trigger, opens the submenu and highlights its first row. Mirrored under RTL. |
| Arrow Left | Inside a submenu, closes it and returns the highlight to the row that opened it. Mirrored under RTL. |
| Home then End | Highlights the first or the last row. |
| A–Z | Typeahead: highlights the first row whose text matches what you type. |
| Enter then Space | Activates the highlighted row. |
| Esc | Closes the menu. Inside a submenu it closes only that submenu, unless closeParentOnEsc is set. |
<ContextMenu.Trigger> renders a plain <div> with no tabindex, no role and
no ARIA, and none of that is an omission: a region that opens a menu on right
click is not a button, and labelling it as one would promise a keyboard
behaviour it does not have. It takes no tab stop of its own, so nothing lands on
it and nothing announces it.
What it does get is the first row of the table. The context-menu key fires a
real contextmenu event on whatever is focused, and that event bubbles — so
a trigger wrapping a link, a button or any other focusable element is reachable
from the keyboard through that element, with no extra work. A trigger wrapping
inert content is not reachable at all. Once the menu is open it is an ordinary
menu and behaves exactly like Menu, roving
tabindex and all.
Theming
The trigger declares three properties, all of them about the ring that marks the
region while its menu is open. Everything else on screen belongs to
Menu — the popup surface, the rows, the separators,
the motion — and is set through the --forte-menu-* knobs on the parts that
declare them.
Like every knob in the library these are declared on the element that owns them,
so an ancestor's value is only inherited and loses. Set them on the trigger
itself through className or a style object; the popup is portalled to
<body>, so an ancestor of the trigger cannot reach it in any case.
| Property | Controls | Default |
|---|---|---|
--forte-context-menu-trigger-ring-color | Colour of the ring that marks the region while its menu is open | var(--forte-color-primary) |
--forte-context-menu-trigger-ring-width | Width of that ring. Set it to 0 to leave the region unmarked | 2px |
--forte-context-menu-trigger-ring-offset | How far the ring sits outside the region's box; negative draws it inside | calc( -1 * var(--forte-context-menu-trigger-ring-width) ) |
API reference
ContextMenu.Root
Groups every part and owns the open state. It renders no DOM element, so it
takes neither className nor ref, and it forwards Base UI's own
ContextMenu.Root props unchanged — open, defaultOpen, onOpenChange,
disabled, orientation, loopFocus, highlightItemOnHover,
closeParentOnEsc, actionsRef and onOpenChangeComplete.
Four of Menu.Root's props are deliberately absent, because there is no trigger
element to hang them on: modal (a context menu is always modal), openOnHover
with its delay and closeDelay, and the handle / triggerId pair that lets
a detached trigger drive a dropdown.
ContextMenu.Trigger
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | The region that answers a right click or a long press — a card, a table row, a canvas, a whole panel. It is ordinary content, not a control: the trigger adds no styling of its own to it beyond marking it while its menu is open. | |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ContextMenuTriggerState> | Replaces the rendered `<div>` with another element or component — `render={<tr />}` for a table row, `render={<li />}` inside a list, so the trigger does not break the parent's content model. |
ContextMenu.Popup
Menu.Popup — the whole floating half of the anatomy (portal, optional
backdrop, positioner and popup) as one part, with the placement defaults
switched to pointer anchoring.
| Prop | Type | Default | Description |
|---|---|---|---|
align | Align | start | How the popup aligns along the chosen side. Menus align to the trigger's start edge rather than its centre, which is Base UI's own default — a centred menu drifts left of a wide trigger and right of a narrow one, and the reading edge is what a list of commands should line up on. |
alignOffset | number | OffsetFunction | 0; -4 inside a `Menu.SubmenuRoot`; 2 inside a `ContextMenu.Root` | Extra offset in pixels along the alignment axis. |
anchor | Element | VirtualElement | RefObject<Element | null> | (() => Element | VirtualElement | null) | null | An element to position the popup against instead of the trigger. | |
backdrop | boolean | false | Render a dimming layer behind the popup. Off by default — a menu is a short-lived list, not a mode — but useful on small screens where the popup covers most of the page. |
children | ReactNode | The items, groups and separators to render. | |
className | string | Additional class name(s) for the popup surface. Applied after the internal styles so consumer utilities win without needing `!important`. | |
collisionPadding | Padding | 5 | Space to keep between the popup and the edge of its collision boundary. |
container | HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null | Render the popup into a different container instead of `<body>`. | |
finalFocus | boolean | RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null) | the trigger | Where focus goes when the menu closes. There is no `initialFocus` counterpart: Base UI moves focus into the popup on open, to the first item for a keyboard open and to the popup itself for a pointer one. |
positionerClassName | string | Additional class name(s) for the positioner — the absolutely positioned wrapper around the popup. This is the only way to reach `--forte-menu-z-index`, the one property declared there. | |
side | Side | "bottom"; "inline-end" inside a `Menu.SubmenuRoot`; the popup is
anchored to the pointer instead inside a `ContextMenu.Root` | Which side of the trigger the popup opens on. Flips automatically to avoid collisions. `"inline-start"` / `"inline-end"` follow writing direction, but Base UI takes that direction from its own `DirectionProvider` context and not from the `dir` attribute — with no provider mounted it is `"ltr"`, so the two resolve to left and right whatever `dir` says. An RTL app has to mount `<DirectionProvider direction="rtl">` for them to mirror. The library's own CSS reads the attribute, so everything else here flips without it. |
sideOffset | number | OffsetFunction | 4; -4 inside a `Menu.SubmenuRoot`; -5 inside a `ContextMenu.Root` | Gap in pixels between the trigger and the popup. |
The rest of the namespace
Item, LinkItem, CheckboxItem, RadioGroup, RadioItem, Group,
GroupLabel, Separator, SubmenuRoot, SubmenuTrigger and Shortcut are
the Menu parts, unchanged. Their props are documented on the Menu
page, and their types are exported under both
names — ContextMenuItemProps is an alias of MenuItemProps, not a second
interface.