Qure UI
Components

Context Menu

The right-click menu — actions attached to a thing rather than to a button.

ACC-4471902 — CT ThoraxRight-click this row
'use client'import { CopyIcon, FileTextIcon, LayersIcon, Trash2Icon } from 'lucide-react'import {  ContextMenu, ContextMenuContent, ContextMenuGroup, ContextMenuItem,  ContextMenuLabel, ContextMenuSeparator, ContextMenuShortcut, ContextMenuTrigger,} from '@/registry/qure/ui/context-menu'export default function ContextMenuDemo() {  return (    <ContextMenu>      <ContextMenuTrigger className="flex h-32 w-80 flex-col items-center justify-center gap-1 rounded-lg border border-dashed border-border text-sm text-muted-foreground">        <span className="font-medium text-foreground">ACC-4471902 — CT Thorax</span>        Right-click this row      </ContextMenuTrigger>      <ContextMenuContent className="min-w-60">        <ContextMenuGroup>          <ContextMenuLabel>ACC-4471902</ContextMenuLabel>          <ContextMenuItem>            <FileTextIcon className="size-4" />            Open report            <ContextMenuShortcut>⇧R</ContextMenuShortcut>          </ContextMenuItem>          <ContextMenuItem>            <LayersIcon className="size-4" />            Compare with prior          </ContextMenuItem>          <ContextMenuItem>            <CopyIcon className="size-4" />            Copy accession number          </ContextMenuItem>        </ContextMenuGroup>        <ContextMenuSeparator />        <ContextMenuItem variant="destructive">          <Trash2Icon className="size-4" />          Remove from worklist        </ContextMenuItem>      </ContextMenuContent>    </ContextMenu>  )}

Installation

npx shadcn@latest add https://qure-ui.qure.ai/r/context-menu.json

Usage

import {
  ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger,
} from '@/components/ui/context-menu'
<ContextMenu>
  <ContextMenuTrigger className="rounded-lg border p-4">
    ACC-4471902 — CT Thorax
  </ContextMenuTrigger>
  <ContextMenuContent>
    <ContextMenuItem>Open report</ContextMenuItem>
    <ContextMenuItem>Compare with prior</ContextMenuItem>
  </ContextMenuContent>
</ContextMenu>

A context menu is an accelerator, never the only route. Right-click is invisible, absent on touch until a long press is discovered, and awkward for anyone driving by keyboard. Every action in here should also exist somewhere a person can see — usually the same list behind a button.

Composition

<ContextMenu>                 {/* owns the open state and the pointer position */}
  <ContextMenuTrigger>        {/* the area that responds to right-click */}
  <ContextMenuContent>        {/* portal + positioner + popup */}
    <ContextMenuLabel>        {/* names what the menu is acting on */}
    <ContextMenuItem>         {/* an action */}
      <ContextMenuItemText>       {/* label column, when there is a description */}
        <ContextMenuDescription>  {/* the second line */}
      <ContextMenuShortcut>       {/* the key hint */}
    <ContextMenuSeparator>
    <ContextMenuCheckboxItem>   {/* a toggle; keeps the menu open */}
    <ContextMenuRadioGroup>     {/* one-of-many, with ContextMenuRadioItem */}
    <ContextMenuSub>            {/* a nested menu */}
      <ContextMenuSubTrigger>
      <ContextMenuSubContent>

The trigger renders a <div>, not a button, because the target is usually a region — a row, a viewport, a thumbnail. That has a consequence worth stating: a <div> is not in the tab order, so if the menu is the only way to reach an action, the region needs a focusable control inside it as well.

ContextMenuLabel throws unless it is inside a ContextMenuGroup or a ContextMenuRadioGroup — "MenuGroupContext is missing", at runtime rather than at compile time. A heading in a menu names a group, so the items it names go inside the group with it.

Everything below the trigger is Base UI's Menu, so the parts, the state attributes and the keyboard behaviour are identical to DropdownMenu. The difference is entirely in what opens it: a right-click or a long press, anchored to the pointer rather than to an element.

On a worklist row

ACC-4471902CT ThoraxSTAT
ACC-4471887Chest X-rayUrgent
ACC-4471851CT HeadRoutine
'use client'import { CheckCircle2Icon, FileTextIcon, UserPlusIcon } from 'lucide-react'import { Badge } from '@/registry/qure/ui/badge'import {  ContextMenu, ContextMenuContent, ContextMenuDescription, ContextMenuItem,  ContextMenuItemText, ContextMenuTrigger,} from '@/registry/qure/ui/context-menu'const rows = [  { acc: 'ACC-4471902', study: 'CT Thorax', priority: 'STAT' },  { acc: 'ACC-4471887', study: 'Chest X-ray', priority: 'Urgent' },  { acc: 'ACC-4471851', study: 'CT Head', priority: 'Routine' },]export default function ContextMenuRow() {  return (    <div className="w-full max-w-md overflow-hidden rounded-lg border border-border">      {rows.map(row => (        <ContextMenu key={row.acc}>          <ContextMenuTrigger className="flex items-center justify-between gap-3 border-b border-border px-4 py-3 text-sm last:border-b-0 hover:bg-muted">            <span className="font-medium">{row.acc}</span>            <span className="text-muted-foreground">{row.study}</span>            <Badge tone={row.priority === 'STAT' ? 'urgent' : 'neutral'}>{row.priority}</Badge>          </ContextMenuTrigger>          <ContextMenuContent className="w-80">            <ContextMenuItem>              <FileTextIcon className="size-4" />              <ContextMenuItemText>                Open {row.acc}                <ContextMenuDescription>{row.study}, with its two priors.</ContextMenuDescription>              </ContextMenuItemText>            </ContextMenuItem>            <ContextMenuItem>              <UserPlusIcon className="size-4" />              <ContextMenuItemText>                Claim this study                <ContextMenuDescription>Moves it out of the shared pool.</ContextMenuDescription>              </ContextMenuItemText>            </ContextMenuItem>            <ContextMenuItem>              <CheckCircle2Icon className="size-4" />              <ContextMenuItemText>                Mark as read                <ContextMenuDescription>No report is generated.</ContextMenuDescription>              </ContextMenuItemText>            </ContextMenuItem>          </ContextMenuContent>        </ContextMenu>      ))}    </div>  )}

Each row is its own ContextMenu, so the menu can name the study it was opened on. A menu that says "Open ACC-4471902" is checkable at a glance; one that says "Open" leaves the reader hoping they right-clicked the row they meant.

In the viewer

Right-click the viewport
'use client'import * as React from 'react'import {  ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup,  ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem,  ContextMenuSeparator, ContextMenuTrigger,} from '@/registry/qure/ui/context-menu'export default function ContextMenuViewer() {  const [overlays, setOverlays] = React.useState({ findings: true, measurements: false })  const [preset, setPreset] = React.useState('lung')  return (    <ContextMenu>      <ContextMenuTrigger className="grid h-40 w-80 place-content-center rounded-lg bg-neutral text-sm text-neutral-foreground">        Right-click the viewport      </ContextMenuTrigger>      <ContextMenuContent className="min-w-60">        <ContextMenuGroup>          <ContextMenuLabel>Overlays</ContextMenuLabel>          <ContextMenuCheckboxItem            checked={overlays.findings}            onCheckedChange={checked => setOverlays(o => ({ ...o, findings: checked }))}          >            qXR findings          </ContextMenuCheckboxItem>          <ContextMenuCheckboxItem            checked={overlays.measurements}            onCheckedChange={checked => setOverlays(o => ({ ...o, measurements: checked }))}          >            Measurements          </ContextMenuCheckboxItem>        </ContextMenuGroup>        <ContextMenuSeparator />        <ContextMenuRadioGroup value={preset} onValueChange={setPreset}>          <ContextMenuLabel>Window preset</ContextMenuLabel>          <ContextMenuRadioItem value="lung">Lung</ContextMenuRadioItem>          <ContextMenuRadioItem value="mediastinum">Mediastinum</ContextMenuRadioItem>          <ContextMenuRadioItem value="bone">Bone</ContextMenuRadioItem>        </ContextMenuRadioGroup>      </ContextMenuContent>    </ContextMenu>  )}

The viewport is where right-click pays for itself: the pointer is already over the image, and overlay and window presets are exactly the settings you want without crossing the screen to a toolbar. Checkbox and radio items keep the menu open, so several can be set in one pass.

Right-click for priors and routing
'use client'import { HistoryIcon, SendIcon } from 'lucide-react'import {  ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator,  ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger,} from '@/registry/qure/ui/context-menu'export default function ContextMenuSubmenu() {  return (    <ContextMenu>      <ContextMenuTrigger className="grid h-32 w-80 place-content-center rounded-lg border border-dashed border-border text-sm text-muted-foreground">        Right-click for priors and routing      </ContextMenuTrigger>      <ContextMenuContent className="min-w-60">        <ContextMenuSub>          <ContextMenuSubTrigger>            <HistoryIcon className="size-4" />            Open a prior          </ContextMenuSubTrigger>          <ContextMenuSubContent className="min-w-60">            <ContextMenuItem>14 Mar 2026 — CT Thorax</ContextMenuItem>            <ContextMenuItem>02 Nov 2025 — CT Thorax</ContextMenuItem>            <ContextMenuItem>19 Jun 2025 — Chest X-ray</ContextMenuItem>            <ContextMenuSeparator />            <ContextMenuItem>Search all priors…</ContextMenuItem>          </ContextMenuSubContent>        </ContextMenuSub>        <ContextMenuSub>          <ContextMenuSubTrigger>            <SendIcon className="size-4" />            Route to          </ContextMenuSubTrigger>          <ContextMenuSubContent className="min-w-52">            <ContextMenuItem>Thoracic MDT</ContextMenuItem>            <ContextMenuItem>Second reader</ContextMenuItem>            <ContextMenuItem disabled>Teaching file — consent pending</ContextMenuItem>          </ContextMenuSubContent>        </ContextMenuSub>      </ContextMenuContent>    </ContextMenu>  )}

One level of nesting, for lists that are genuinely lists — the priors for this patient, the teams a study can be routed to.

Where it opens

The menu is anchored to the pointer, so side and align are measured from the click, not from the trigger. The defaults — side="right", align="start", a 4px alignOffset — put the top left corner just below and right of the cursor, and it flips near an edge.

The trigger sets user-select: none. Without it, a right-click drag selects the accession number underneath the menu, and the selection is still sitting there after the menu closes.

Keyboard

There is a keyboard route in: the Menu key, or Shift+F10, opens the menu on the focused element. Once open it behaves like any other menu — arrows to move, Enter to act, Right and Left for submenus, Escape to close.

API Reference

Everything Base UI's Context Menu accepts. What we add on ContextMenuItem:

Prop

Type

On ContextMenuContent:

Prop

Type

ContextMenuRoot takes the Menu root's props minus the ones a context menu cannot have — modal, handle, triggerId, openOnHover and the hover delays. There is no trigger element to hover, and the pointer position replaces the trigger id.

On this page