ContextMenu
An actions menu opened with a right click (or a long press) — same API and look as DropdownMenu.
'use client';
import { ContextMenu, toast } from '@akhatmirzo/ui';
import { Copy, Download, FileText, Pencil, Trash2 } from 'lucide-react';
export default function ContextMenuBasic() {
return (
<ContextMenu>
<ContextMenu.Trigger asChild>
<div className="flex w-72 items-center gap-3 rounded-xl border border-dashed border-border-strong bg-surface p-4 select-none">
<span className="grid size-10 place-items-center rounded-lg bg-primary-soft text-primary-soft-foreground">
<FileText className="size-5" aria-hidden="true" />
</span>
<span className="flex flex-col">
<span className="text-sm font-medium">Hisobot-2026.pdf</span>
<span className="text-xs text-text-muted">O'ng tugma bilan bosing</span>
</span>
</div>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item
icon={<Pencil />}
shortcut={['mod', 'E']}
onSelect={() => toast('Tahrirlash')}
>
Tahrirlash
</ContextMenu.Item>
<ContextMenu.Item
icon={<Copy />}
shortcut={['mod', 'D']}
onSelect={() => toast('Nusxa olindi')}
>
Nusxa olish
</ContextMenu.Item>
<ContextMenu.Item icon={<Download />} onSelect={() => toast('Yuklanmoqda')}>
Yuklab olish
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item icon={<Trash2 />} variant="danger" onSelect={() => toast("O'chirildi")}>
O'chirish
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu>
);
}
Import
import { ContextMenu } from '@akhatmirzo/ui';When to use
- Quick actions on a file, card or table row — desktop users expect the right click.
- Only as an extra path. Not everyone discovers right-click menus, and on phones they open with
a long press. Make important actions available elsewhere too — a button or a
DropdownMenu. - Need a menu that opens on a button click? Use
DropdownMenu. Both share the same parts, so you can write the actions once and use them in both places.
Anatomy
Parts, props and styles match DropdownMenu (the styles come from a shared
module). The difference: Trigger is not a button but the area you right-click, and the panel
opens at the pointer.
| Part | What it does |
|---|---|
ContextMenu | State: onOpenChange, modal, dir |
ContextMenu.Trigger | The area (an unstyled <span>; usually with asChild) |
ContextMenu.Content | The panel: portal, opens at the pointer, looping nav |
ContextMenu.Item | An action: icon, shortcut, variant, onSelect |
ContextMenu.CheckboxItem | A toggleable item (✓) |
ContextMenu.RadioGroup / RadioItem | One choice out of several (●) |
ContextMenu.Label / Group / Separator | Heading, group and divider |
ContextMenu.Sub / SubTrigger / SubContent | A submenu |
<ContextMenu>
<ContextMenu.Trigger asChild>
<Card>…file…</Card>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item icon={<Pencil />} shortcut={['mod', 'E']}>
Edit
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item variant="danger">Delete</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu>Checkable items
CheckboxItem (checked / onCheckedChange) and RadioGroup (value / onValueChange, or
defaultValue) work well for view settings.
'use client';
import { ContextMenu, cn } from '@akhatmirzo/ui';
import { useState } from 'react';
export default function ContextMenuCheckboxRadio() {
const [grid, setGrid] = useState(true);
const [rulers, setRulers] = useState(false);
const [zoom, setZoom] = useState('100');
return (
<ContextMenu>
<ContextMenu.Trigger asChild>
<div
className={cn(
'grid h-48 w-full max-w-md place-items-center rounded-xl border border-border bg-surface text-sm text-text-muted select-none',
grid && 'border-dashed',
rulers && 'border-l-4 border-t-4',
)}
>
Kanvas · {zoom}% — o'ng tugma bilan bosing
</div>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Label>Ko'rinish</ContextMenu.Label>
<ContextMenu.CheckboxItem checked={grid} onCheckedChange={setGrid}>
Panjara
</ContextMenu.CheckboxItem>
<ContextMenu.CheckboxItem checked={rulers} onCheckedChange={setRulers}>
Chizg'ichlar
</ContextMenu.CheckboxItem>
<ContextMenu.Separator />
<ContextMenu.Label>Masshtab</ContextMenu.Label>
<ContextMenu.RadioGroup value={zoom} onValueChange={setZoom}>
<ContextMenu.RadioItem value="50">50%</ContextMenu.RadioItem>
<ContextMenu.RadioItem value="100">100%</ContextMenu.RadioItem>
<ContextMenu.RadioItem value="200">200%</ContextMenu.RadioItem>
</ContextMenu.RadioGroup>
</ContextMenu.Content>
</ContextMenu>
);
}
Table row: two ways in
Right-click a row for the ContextMenu, or use the "⋯" button for a DropdownMenu with the same
actions. Since the part names match, the actions live in a single component. The submenu opens with
→ or on hover.
- #1042Dilnoza Karimova1 250 000 so'm
- #1043Javohir Toshmatov480 000 so'm
'use client';
import { ContextMenu, DropdownMenu, IconButton, toast } from '@akhatmirzo/ui';
import { Ellipsis, Pencil, Send, Trash2 } from 'lucide-react';
const orders = [
{ id: '#1042', customer: 'Dilnoza Karimova', total: "1 250 000 so'm" },
{ id: '#1043', customer: 'Javohir Toshmatov', total: "480 000 so'm" },
];
/** ContextMenu va DropdownMenu qismlari bir xil — amallar bir marta yoziladi */
function Actions({ id, Menu }: { id: string; Menu: typeof ContextMenu | typeof DropdownMenu }) {
return (
<>
<Menu.Item icon={<Pencil />} onSelect={() => toast(`${id} tahrirlanmoqda`)}>
Tahrirlash
</Menu.Item>
<Menu.Sub>
<Menu.SubTrigger icon={<Send />}>Yuborish</Menu.SubTrigger>
<Menu.SubContent>
<Menu.Item onSelect={() => toast('Telegram')}>Telegram</Menu.Item>
<Menu.Item onSelect={() => toast('Email')}>Email</Menu.Item>
</Menu.SubContent>
</Menu.Sub>
<Menu.Separator />
<Menu.Item icon={<Trash2 />} variant="danger" onSelect={() => toast(`${id} o'chirildi`)}>
O'chirish
</Menu.Item>
</>
);
}
export default function ContextMenuTableRow() {
return (
<ul className="m-0 w-full max-w-lg list-none rounded-xl border border-border bg-surface p-0">
{orders.map((o) => (
<ContextMenu key={o.id}>
<ContextMenu.Trigger asChild>
<li className="flex items-center gap-3 border-b border-border px-4 py-2 text-sm last:border-b-0">
<span className="font-medium">{o.id}</span>
<span className="truncate text-text-muted">{o.customer}</span>
<span className="ml-auto tabular-nums">{o.total}</span>
{/* Xuddi shu amallar tugma orqali ham mavjud */}
<DropdownMenu>
<DropdownMenu.Trigger asChild>
<IconButton size="sm" variant="ghost" aria-label={`${o.id} amallari`}>
<Ellipsis />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<Actions id={o.id} Menu={DropdownMenu} />
</DropdownMenu.Content>
</DropdownMenu>
</li>
</ContextMenu.Trigger>
<ContextMenu.Content>
<Actions id={o.id} Menu={ContextMenu} />
</ContextMenu.Content>
</ContextMenu>
))}
</ul>
);
}
On phones
On touch screens the menu opens with a long press. That gesture is hidden, so offer the actions through a visible button as well.
Props
ContextMenu
The Radix ContextMenu Root props: onOpenChange, modal (defaults to true) and dir. The
menu opens only from a user gesture.
ContextMenu.Trigger
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Uses the child (a card, a row) as the area |
disabled | boolean | — | true — the menu does not open |
ContextMenu.Content
Also accepts the rest of the Radix ContextMenu.Content props (alignOffset, onCloseAutoFocus,
onEscapeKeyDown ...), <div> attributes and ref.
| Prop | Type | Default | Description |
|---|---|---|---|
collisionPadding | number | 8 | Minimum distance from the screen edge, px |
loop | boolean | true | ↑ ↓ wrap around from the last item to the first |
container | HTMLElement | null | — | Portal container (defaults to document.body) |
ContextMenu.Item
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | — | Leading icon (16px) |
shortcut | string[] | — | A Kbd label on the right, e.g. ['mod', 'E'] |
variant | 'default' | 'danger' | 'default' | danger — red text and highlight |
onSelect | (event: Event) => void | — | Called on select. preventDefault() keeps the menu open |
disabled | boolean | — | Disables the item |
asChild | boolean | false | Renders the child (e.g. an <a>) as the item |
The props of CheckboxItem, RadioGroup, RadioItem, Sub, SubTrigger and SubContent are
the same as in DropdownMenu.
Data attributes
| Attribute | Where | Value |
|---|---|---|
data-state | trigger, panel | open · closed |
data-variant | item | default · danger |
data-highlighted | item | under the keyboard or pointer |
data-disabled | item, trigger | present when disabled |
data-state | Checkbox/RadioItem | checked · unchecked |
Keyboard
| Key | Action |
|---|---|
Shift + F10 | Opens the menu on the focused area (browser) |
↓ / ↑ | Move between items (wraps around) |
Home / End | First / last item |
| Letter | Jump to the item starting with that letter |
→ / ← | Open / close a submenu |
Enter / Space | Select and close |
Esc | Close |
Accessibility
- Roles. The panel is
role="menu", items aremenuitem,menuitemcheckbox,menuitemradio. Built on Radix ContextMenu. - Hard to discover. A right-click menu is invisible to screen reader and keyboard users — always provide important actions another way too.
- Shortcuts. With
shortcut, the item getsaria-keyshortcuts; the visibleKbdis hidden from screen readers. The label does not listen for the key — wire it up withuseHotkeys. - Icons are
aria-hidden.