@xsolla/xui-context-menu
v0.162.0
Published
## Overview
Readme
Context Menu
Overview
ContextMenu is an anchored panel of selectable cells. The component is built around two primitives: a single ContextMenuItem whose type prop switches between cell variants (option, search, heading, divider), and a panel that supports a preset shorthand (type="list" | "phone" | "checkbox" | "radio" | "status" | "brandLogo" | "avatar" | "loading") plus a fully custom composition path via children.
When to use
- A primary action menu attached to a trigger (e.g. a button's overflow actions).
- A selection control with checkbox or radio cells (multi-select or single-select).
- A lightweight picker for status, country, or brand-logo lists.
When not to use
- For form fields driven by validation — use
Select,Autocompleteor a plainInputinstead. - For navigational menus or app-level navigation — use the appropriate navigation primitives.
- For a single destructive confirmation — use a
Dialog.
Installation
yarn add @xsolla/xui-context-menuTwo API paths
Preset path
Pass type and items. The panel renders the preset's chrome and composes each option with the right control or slot.
import { ContextMenu } from "@xsolla/xui-context-menu";
<ContextMenu
type="list"
trigger={<Button>Open</Button>}
items={[
{ type: "option", label: "Edit" },
{ type: "option", label: "Duplicate" },
{ type: "option", label: "Delete", destructive: true },
]}
/>;Custom path
Compose cells as children when you need full control of the layout, want to mix headings and dividers freely, or render a slot the preset path doesn't cover.
import { ContextMenu, ContextMenuItem } from "@xsolla/xui-context-menu";
<ContextMenu trigger={<Button>Open</Button>} aria-label="Actions">
<ContextMenuItem type="heading" label="Workspace" />
<ContextMenuItem type="option" label="Personal" />
<ContextMenuItem type="option" label="Acme Inc." />
<ContextMenuItem type="divider" />
<ContextMenuItem type="option" label="Sign out" destructive />
</ContextMenu>;Choose the preset path for typical menus where the data is uniform; choose the custom path when cells differ structurally or when you need to drop in bespoke nodes between cells.
ContextMenuItem reference
ContextMenuItem is a discriminated union on type. All cell types accept size, data-testid and theme-override props.
type="option"
| Prop | Type | Purpose |
| --- | --- | --- |
| label | ReactNode | Primary cell text (required). |
| description | ReactNode | Secondary line beneath the label. |
| leadingControl | "checkbox" \| "radio" | Renders a Checkbox or Radio at the start. |
| leadingIcon | ReactNode | Icon node before the label group. |
| status | ReactNode | Status indicator slot (e.g. <Status>). |
| iconWrapper | ReactNode | Wrapped icon / avatar slot. |
| slot / slotContent | ReactNode | Generic slot before the label. |
| value | ReactNode | Right-side primary text (e.g. shortcut value, dial code). |
| hint | ReactNode | Right-side secondary text below value. |
| trailingIcon | ReactNode | Trailing icon at the end of the cell. |
| keyboardShortcut | string | Display-only shortcut rendered as <kbd> and exposed via aria-keyshortcuts. |
| hasSubmenu | boolean | Marks the cell as a submenu trigger and renders a chevron. |
| submenu | ReactNode | A nested <ContextMenu> opened on hover/ArrowRight/Enter. |
| checked | boolean | Fully controlled checked state. |
| disabled | boolean | Disables interaction and applies the disabled style. |
| destructive | boolean | Applies the destructive content colour. |
| onSelect | () => void | Fires on activation (click, Enter, Space). |
| onCheckedChange | (checked: boolean) => void | Optional change callback for controls. |
Render order (left → right): leadingControl, leadingIcon, status, iconWrapper, slotContent, label (with optional description below), value (with optional hint below), keyboardShortcut, submenu chevron, trailingIcon.
type="search"
| Prop | Type | Purpose |
| --- | --- | --- |
| value | string | Controlled value (required). |
| onValueChange | (value: string) => void | Change callback (required). |
| placeholder | string | Defaults to "Search". |
| autoFocus | boolean | Focuses the input on mount. |
| aria-label | string | Defaults to "Search options". |
type="heading"
| Prop | Type | Purpose |
| --- | --- | --- |
| label | ReactNode | Section title (uppercase styling). |
| description | ReactNode | Optional helper line beneath. |
type="divider"
A horizontal rule with role="separator". No content props.
ContextMenu reference
| Prop | Type | Purpose |
| --- | --- | --- |
| type | "list" \| "loading" \| "phone" \| "checkbox" \| "status" \| "brandLogo" \| "radio" \| "avatar" | Panel preset; works with items. |
| items | ReadonlyArray<Option \| Heading \| Divider> | Data-driven cells for the preset path. |
| children | ReactNode | Custom-composition cells (alternative to items). |
| size | "sm" \| "md" \| "lg" \| "xl" | Controls cell sizing across the panel. Default md. |
| searchable | boolean | Auto-renders a sticky search cell and filters options. |
| loading | boolean | Renders a centred spinner instead of the cell list. |
| emptyMessage | string | Custom message for the default empty state. |
| empty | ReactNode | Replace the empty state entirely. |
| trigger | ReactNode | Element that toggles the panel; receives aria-haspopup / aria-expanded. |
| placement | "bottom-start" \| "top-start" \| "bottom-end" \| "top-end" | Initial placement. Auto-flips when clipped. |
| isOpen | boolean | Controlled open state. |
| onOpenChange | (open: boolean) => void | Open-state callback. |
| closeOnSelect | boolean | Override the per-preset default. |
| width | number | Forces panel width (px). |
| maxHeight | number | Caps panel height (px); body scrolls and search stays sticky. |
| onSelect | (item: ContextMenuOptionItemProps) => void | Fires for the preset path on option activation. |
| aria-label | string | Accessible name for the menu container. |
| data-testid | string | Testing handle. |
Behaviour & accessibility
- The panel root is
role="menu"(or hostsrole="menuitemcheckbox"/role="menuitemradio"cells whencheckedis provided). Headings render asrole="presentation", dividers asrole="separator", and the search cell asrole="searchbox". closeOnSelectdefaults totruefor every preset exceptcheckbox, where multi-select keeps the panel open.- On open, focus moves to the search input when present, otherwise to the first option.
- On close, focus returns to the trigger.
- The trigger element receives
aria-haspopup="menu"andaria-expandedsynced to the open state.
Keyboard reference
| Key | Action |
| --- | --- |
| ↑ / ↓ | Move active option up/down (skips heading/divider). |
| Enter / Space | Activate the focused option. |
| Esc | Close the menu and return focus to the trigger. |
| Tab | Close the menu and continue natural focus order. |
| Home / End | Jump to the first/last option. |
| → / Enter | Open a submenu when on a hasSubmenu option. |
| ← / Esc | Close the submenu and return focus to its parent option. |
Content guidelines
- Use short, imperative labels ("Edit", "Duplicate", "Sign out").
- Use sentence case for option labels.
- Use uppercase for headings — the heading cell already applies the visual treatment.
- Place destructive options at the bottom of the list, ideally separated by a divider.
- Prefer specific empty messages ("No countries match") over the generic default.
- Aim for seven options or fewer per panel; group with headings or split into submenus when longer.
Migration from prior API
| Old | New |
| --- | --- |
| ContextMenuCheckboxItem | <ContextMenuItem type="option" leadingControl="checkbox" /> |
| ContextMenuRadioItem | <ContextMenuItem type="option" leadingControl="radio" /> |
| ContextMenuRadioGroup | type="radio" panel preset with shared selected state |
| ContextMenuGroup | <ContextMenuItem type="heading" /> |
| ContextMenuSeparator | <ContextMenuItem type="divider" /> |
| ContextMenuSearch | <ContextMenuItem type="search" /> (or set searchable: true on the panel for auto-render) |
| Size scale s / m / l / xl | sm / md / lg / xl |
