RightClickMenu
Opens a menu of actions where the user right-clicks.
Installation
- Install the package.
npm install @hashintel/ds-components- Import the component.
import { RightClickMenu } from "@hashintel/ds-components";
export const EntityCard = ({ entityId }: { entityId: string }) => (
<RightClickMenu
items={[
{ text: "Open entity", onClick: () => openEntity(entityId) },
{ text: "Duplicate", onClick: () => duplicateEntity(entityId) },
]}
>
<article>{/* the right-clickable area */}</article>
</RightClickMenu>
);When to use
- Do: Use RightClickMenu as a shortcut to actions on a specific object — a table row, graph node, file, or canvas region.
- Do: Use it on large surfaces (canvases, editors) where a persistent toolbar for every object would add noise.
When not to use
- Don't: Avoid making it the only path to an action — right-click is invisible until tried, and many users never try.
- Don't: Don't wrap text inputs or selectable prose — it suppresses the browser's native context menu (copy, paste, spell-check) and disables text selection in the area.
Anatomy
The wrapped children are the right-clickable area; items describes the menu
rows using the same items model as Menu — actions, links, submenus, and
labelled groups:
<RightClickMenu
items={[
// an action — onClick receives the item's id (defaults to its text)
{ text: "Open entity", onClick: openEntity },
// a link
{ text: "Open in new tab", href: entityUrl, target: "_blank" },
// a group draws dividers; an empty label makes it a plain divider
{
id: "danger",
label: "",
items: [{ text: "Delete entity", tone: "error", onClick: confirmDelete }],
},
]}
>
<EntityCard /> {/* the right-clickable area */}
</RightClickMenu>Examples
Basic
Pass items with an onClick per action and wrap the area the menu belongs
to. Right-click (or long-press on touch) anywhere inside it opens the menu at
the pointer.
Grouped actions
Wrap related items in { id, label, items } groups — dividers draw between
groups automatically. Pass an empty label for a divider without a heading.
Icons and shortcut hints
An item's icon reinforces its label; suffix right-aligns extra content,
conventionally the keyboard shortcut. The suffix only displays the hint — the
shortcut itself must be registered elsewhere.
Disabled items
Disable actions that don't currently apply instead of removing them — a stable list keeps the menu predictable between opens. Disabled items are skipped by keyboard navigation.
Destructive actions
Set tone: "error" on destructive items and keep them last, in a group of
their own, so a slipped pointer doesn't land on them. Confirm irreversible
actions before running them.
Submenus
Give an item subItems to open a nested menu on hover or ArrowRight. Keep
nesting to one level — deeper trees are hard to traverse with a pointer.
Checkable view options
Mark stateful rows with selected and selectedStyle: "checkbox";
keepOpenOnSelect keeps the menu up while the user toggles several. Rebuild
items from state — the menu renders them fresh on every open.
import { useState } from "react";
import { RightClickMenu } from "@hashintel/ds-components";
export const GraphCanvas = () => {
const [showGrid, setShowGrid] = useState(true);
const [snapToGrid, setSnapToGrid] = useState(false);
return (
<RightClickMenu
items={[
{
text: "Show grid",
selected: showGrid,
selectedStyle: "checkbox",
keepOpenOnSelect: true,
onClick: () => setShowGrid((previous) => !previous),
},
{
text: "Snap to grid",
selected: snapToGrid,
selectedStyle: "checkbox",
keepOpenOnSelect: true,
onClick: () => setSnapToGrid((previous) => !previous),
},
]}
>
<section>{/* canvas */}</section>
</RightClickMenu>
);
};Link items
Items with href render real anchors — right for "Open in new tab" rows.
target: "_blank" is the only target supported.
Placement
The menu opens at the pointer; position sets the preferred side and
alignment relative to it, and another placement is chosen when the preferred
one doesn't fit.
Behavior
- The menu opens on right-click, or a long press from touch and pen pointers. Opening suppresses the browser's native context menu inside the area.
- Menu contents mount on open and unmount on close, so
itemsbuilt from current state are always fresh.
Content
- Write item labels verb-first, 1–3 words, sentence case: "Rename", "Copy entity ID", "Delete entity". No punctuation.
- Show only actions relevant to the object, most-used first — a context menu is a shortcut, not a command reference. Keep it under ~10 items per level, grouping once you pass 5.
- Keep group labels to one or two category words: "View", "Organize".
Accessibility
RightClickMenu implements the WAI-ARIA menu pattern.
What RightClickMenu provides
- Menu semantics, highlight-based arrow-key navigation, typeahead, and Escape dismissal.
- Long-press opening on touch and pen pointers, so the menu isn't mouse-only.
What you must do
- Make the wrapped area focusable (or ensure it contains a focusable element) so keyboard users can reach it and open the menu.
API reference
RightClickMenu
| Prop | Type | Description |
|---|---|---|
| items | ItemOrGroup<MenuItem>[] | |
| children | ReactNode | The area to attach the context menu to. Right-clicking (or long-pressing on touch/pen) anywhere within the rendered children opens the menu. |
| position | Position default: "bottom-start" | The preferred position of the menu relative to the pointer it opens at; another may be chosen for a better fit. |
MenuItem
The shape of items and subItems entries — the same items model as Menu.
Each takes text plus exactly one of onClick, href, or subItems — or a
lone custom node for a presentational row.
| Prop | Type | Description |
|---|---|---|
| description | ReactNode | One line of secondary text under the label |
| icon | "function" | "filter" | "at" | "circle" | … 137 more | Design-system icon before the label — use sparingly, to differentiate important items |
| loading | boolean | Shows a spinner and makes the item inert |
| suffix | ReactNode | Trailing content aligned to the row's end, e.g. a count |
| indent | number | Indents the row by this many steps |
| disabled | boolean | Disables the item; arrow keys skip it |
| tone | "neutral" | "brand" | "error" | error marks destructive actions |
| selectedStyle | "checkbox" | "tick" | "highlight" default: "highlight" | How a selected item is shown |
| selectedTone | "neutral" | "brand" | "error" | Tone of the selected indicator, when it differs from tone |
| keepOpenOnSelect | boolean | Keeps the menu open when this item is selected |
| id | string | Identifies the item, e.g. in onClick — required when text is not a plain string Identifies the item, e.g. in onClick — a plain-string text doubles as the id |
| text | ReactNode | The row's label The row's label; a plain string doubles as the item's id |
| href | string | Renders the item as a link to this URL — one of href, onClick, or subItems per item |
| target | "_blank" | Opens the link in a new tab |
| onClick | ((id: string) => void) | Called with the item's id when it is activated |
| subItems | ItemOrGroup<Item>[] | Opens a nested submenu of these entries |
| menuClassName | string | Applied to this item's submenu content, composed with the list's own class, for styling that only that submenu should carry. |
| custom | ReactNode | A row of arbitrary content — never highlighted or selectable, though Tab reaches focusable children. Mutually exclusive with every other field. |
| selected | boolean | Whether the item is marked as the current choice. |
MenuItemGroup
A labelled group of items, usable wherever items are: { id, label, items }.
| Prop | Type | Description |
|---|---|---|
| id | string | Identifies the group |
| label | ReactNode | Rendered as an uppercase label above the group's items |
| items | MenuItem[] | The group's entries — items and custom rows; groups don't nest |
Was this page helpful?