@xsolla/xui-context-menu
v0.112.0
Published
A cross-platform React context menu component that can be triggered by a button or right-click, supporting various item types including checkboxes and radio buttons.
Readme
Context Menu
A cross-platform React context menu component that can be triggered by a button or right-click, supporting various item types including checkboxes and radio buttons.
Installation
npm install @xsolla/xui-context-menu
# or
yarn add @xsolla/xui-context-menuDemo
Basic Context Menu
import * as React from 'react';
import { ContextMenu } from '@xsolla/xui-context-menu';
import { Button } from '@xsolla/xui-button';
export default function BasicContextMenu() {
return (
<ContextMenu
trigger={<Button>Open Menu</Button>}
list={[
{ id: 'edit', label: 'Edit' },
{ id: 'duplicate', label: 'Duplicate' },
{ id: 'delete', label: 'Delete' },
]}
onSelect={(item) => console.log('Selected:', item.id)}
/>
);
}Compound Component API
import * as React from 'react';
import { ContextMenu } from '@xsolla/xui-context-menu';
import { Button } from '@xsolla/xui-button';
export default function CompoundAPI() {
return (
<ContextMenu trigger={<Button>Actions</Button>}>
<ContextMenu.Item onPress={() => console.log('Edit')}>Edit</ContextMenu.Item>
<ContextMenu.Item onPress={() => console.log('Copy')}>Copy</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item onPress={() => console.log('Delete')}>Delete</ContextMenu.Item>
</ContextMenu>
);
}With Groups
import * as React from 'react';
import { ContextMenu } from '@xsolla/xui-context-menu';
import { Button } from '@xsolla/xui-button';
export default function WithGroups() {
return (
<ContextMenu
trigger={<Button>Menu with Groups</Button>}
groups={[
{
id: 'file',
label: 'File',
items: [
{ id: 'new', label: 'New' },
{ id: 'open', label: 'Open' },
{ id: 'save', label: 'Save' },
],
},
{
id: 'edit',
label: 'Edit',
items: [
{ id: 'cut', label: 'Cut' },
{ id: 'copy', label: 'Copy' },
{ id: 'paste', label: 'Paste' },
],
},
]}
/>
);
}Anatomy
import { ContextMenu } from '@xsolla/xui-context-menu';
<ContextMenu
trigger={<Button>Menu</Button>} // Trigger element
isOpen={isOpen} // Controlled open state
onOpenChange={setIsOpen} // Open state callback
list={items} // Data-driven items
groups={groups} // Grouped items
size="md" // Size variant
width={200} // Menu width
maxHeight={300} // Max height with scroll
closeOnSelect={true} // Close after selection
onSelect={handleSelect} // Selection callback
onCheckedChange={handleChecked} // Checkbox/radio callback
/>Examples
Checkbox Items
import * as React from 'react';
import { ContextMenu } from '@xsolla/xui-context-menu';
import { Button } from '@xsolla/xui-button';
export default function CheckboxItems() {
const [settings, setSettings] = React.useState({
notifications: true,
sound: false,
autoSave: true,
});
return (
<ContextMenu
trigger={<Button>Settings</Button>}
list={[
{ id: 'notifications', label: 'Notifications', variant: 'checkbox', checked: settings.notifications },
{ id: 'sound', label: 'Sound', variant: 'checkbox', checked: settings.sound },
{ id: 'autoSave', label: 'Auto Save', variant: 'checkbox', checked: settings.autoSave },
]}
onCheckedChange={(id, checked) => {
setSettings((prev) => ({ ...prev, [id]: checked }));
}}
/>
);
}Radio Group
import * as React from 'react';
import { ContextMenu } from '@xsolla/xui-context-menu';
import { Button } from '@xsolla/xui-button';
export default function RadioGroupMenu() {
const [theme, setTheme] = React.useState('light');
return (
<ContextMenu trigger={<Button>Theme: {theme}</Button>}>
<ContextMenu.Group label="Theme">
<ContextMenu.RadioGroup value={theme} onValueChange={setTheme}>
<ContextMenu.RadioItem value="light">Light</ContextMenu.RadioItem>
<ContextMenu.RadioItem value="dark">Dark</ContextMenu.RadioItem>
<ContextMenu.RadioItem value="system">System</ContextMenu.RadioItem>
</ContextMenu.RadioGroup>
</ContextMenu.Group>
</ContextMenu>
);
}With Search
import * as React from 'react';
import { ContextMenu } from '@xsolla/xui-context-menu';
import { Button } from '@xsolla/xui-button';
export default function WithSearch() {
const [search, setSearch] = React.useState('');
const allItems = [
{ id: 'apple', label: 'Apple' },
{ id: 'banana', label: 'Banana' },
{ id: 'cherry', label: 'Cherry' },
{ id: 'date', label: 'Date' },
{ id: 'elderberry', label: 'Elderberry' },
];
const filteredItems = allItems.filter((item) =>
item.label.toLowerCase().includes(search.toLowerCase())
);
return (
<ContextMenu trigger={<Button>Select Fruit</Button>}>
<ContextMenu.Search
value={search}
onValueChange={setSearch}
placeholder="Search fruits..."
/>
{filteredItems.map((item) => (
<ContextMenu.Item key={item.id}>{item.label}</ContextMenu.Item>
))}
</ContextMenu>
);
}With Icons and Shortcuts
import * as React from 'react';
import { ContextMenu } from '@xsolla/xui-context-menu';
import { Button } from '@xsolla/xui-button';
import { Copy, Scissors, Clipboard } from '@xsolla/xui-icons-base';
export default function WithIconsAndShortcuts() {
return (
<ContextMenu
trigger={<Button>Edit</Button>}
list={[
{ id: 'cut', label: 'Cut', icon: <Scissors />, trailing: { type: 'shortcut', content: 'Cmd+X' } },
{ id: 'copy', label: 'Copy', icon: <Copy />, trailing: { type: 'shortcut', content: 'Cmd+C' } },
{ id: 'paste', label: 'Paste', icon: <Clipboard />, trailing: { type: 'shortcut', content: 'Cmd+V' } },
]}
/>
);
}Right-Click Context Menu
import * as React from 'react';
import { ContextMenu } from '@xsolla/xui-context-menu';
export default function RightClickMenu() {
const [position, setPosition] = React.useState<{ x: number; y: number } | null>(null);
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<div
onContextMenu={handleContextMenu}
style={{ width: 300, height: 200, background: '#f0f0f0', padding: 16 }}
>
Right-click anywhere in this area
{position && (
<ContextMenu
isOpen={!!position}
onOpenChange={(open) => !open && setPosition(null)}
position={position}
list={[
{ id: 'inspect', label: 'Inspect' },
{ id: 'refresh', label: 'Refresh' },
]}
/>
)}
</div>
);
}API Reference
ContextMenu
ContextMenuProps:
| Prop | Type | Default | Description |
| :--- | :--- | :------ | :---------- |
| children | ReactNode | - | Compound component children. |
| trigger | ReactNode | - | Element that triggers the menu. |
| list | ContextMenuItemData[] | - | Data-driven item list. |
| groups | ContextMenuGroupData[] | - | Grouped items with labels. |
| isOpen | boolean | - | Controlled open state. |
| onOpenChange | (open: boolean) => void | - | Open state change callback. |
| position | { x: number; y: number } | - | Fixed position for right-click menus. |
| size | "sm" \| "md" \| "lg" | "md" | Menu size variant. |
| width | number | - | Menu width in pixels. |
| maxHeight | number | 300 | Max height before scrolling. |
| closeOnSelect | boolean | true | Close menu after item selection. |
| isLoading | boolean | false | Show loading spinner. |
| onSelect | (item: ContextMenuItemData) => void | - | Item selection callback. |
| onCheckedChange | (id: string, checked: boolean) => void | - | Checkbox/radio change callback. |
| aria-label | string | - | Accessible menu label. |
ContextMenuItemData:
interface ContextMenuItemData {
id: string;
label: string;
icon?: ReactNode;
description?: string;
disabled?: boolean;
selected?: boolean;
checked?: boolean;
variant?: 'default' | 'checkbox' | 'radio';
trailing?: { type: 'shortcut' | 'content' | 'none'; content?: string | ReactNode };
children?: ContextMenuItemData[];
onPress?: () => void;
}Compound Components
| Component | Description |
| :-------- | :---------- |
| ContextMenu.Item | Standard menu item. |
| ContextMenu.CheckboxItem | Item with checkbox. |
| ContextMenu.RadioGroup | Container for radio items. |
| ContextMenu.RadioItem | Radio button item. |
| ContextMenu.Group | Group with optional label. |
| ContextMenu.Separator | Visual separator line. |
| ContextMenu.Search | Search input for filtering. |
Keyboard Navigation
| Key | Action |
| :-- | :----- |
| ArrowDown | Move to next item |
| ArrowUp | Move to previous item |
| Home | Move to first item |
| End | Move to last item |
| Enter / Space | Select current item |
| Escape | Close menu |
| Tab | Close menu |
Accessibility
- Menu has
role="menu"with proper ARIA attributes - Items have
role="menuitem", checkboxes haverole="menuitemcheckbox" - Keyboard navigation follows WAI-ARIA menu pattern
- Focus is trapped within menu when open
- Escape key closes menu and returns focus to trigger
