DropdownMenu
An actions menu with icons, keyboard shortcut labels, checkable items and submenus.
'use client';
import { Button, DropdownMenu, toast } from '@akhatmirzo/ui';
import { Archive, ChevronDown, Copy, Pencil, Trash2 } from 'lucide-react';
export default function DropdownMenuBasic() {
return (
<DropdownMenu>
<DropdownMenu.Trigger asChild>
<Button variant="outline" rightIcon={<ChevronDown />}>
Amallar
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item
icon={<Pencil />}
shortcut={['mod', 'E']}
onSelect={() => toast('Tahrirlash')}
>
Tahrirlash
</DropdownMenu.Item>
<DropdownMenu.Item
icon={<Copy />}
shortcut={['mod', 'D']}
onSelect={() => toast('Nusxa olindi')}
>
Nusxa olish
</DropdownMenu.Item>
<DropdownMenu.Item icon={<Archive />} onSelect={() => toast('Arxivlandi')}>
Arxivlash
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item icon={<Trash2 />} variant="danger" onSelect={() => toast("O'chirildi")}>
O'chirish
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
);
}
Import
import { DropdownMenu } from '@akhatmirzo/ui';When to use
- Actions on a single object — the "⋯" button in a table row, a card menu, a profile menu.
- View options —
CheckboxItem(which columns to show) andRadioGroup(sort order). - Not for picking a form value. For form fields use
SelectorCombobox— they hold the value and submit it with the form. - Destructive actions (
variant="danger") go last, after a separator; confirm them withconfirm()when needed. - Keep submenus one level deep — deeper nesting is awkward with a keyboard and on phones.
Anatomy
| Part | What it does |
|---|---|
DropdownMenu | State: open, defaultOpen, onOpenChange |
DropdownMenu.Trigger | Opens the menu. Without asChild, it looks like an outline button |
DropdownMenu.Content | The panel: portal, positioning, looping navigation |
DropdownMenu.Item | An action: icon, shortcut, variant, onSelect |
DropdownMenu.CheckboxItem | A toggleable item (✓) |
DropdownMenu.RadioGroup / RadioItem | One choice out of several (●) |
DropdownMenu.Label | A group heading |
DropdownMenu.Group | Groups items logically |
DropdownMenu.Separator | A divider line |
DropdownMenu.Sub / SubTrigger / SubContent | A submenu |
Checkable items
CheckboxItem uses checked / onCheckedChange; RadioGroup uses value / onValueChange
(or an uncontrolled defaultValue). The menu closes after a pick; to let users toggle several
items in a row, call e.preventDefault() inside onSelect.
'use client';
import { DropdownMenu } from '@akhatmirzo/ui';
import { useState } from 'react';
export default function DropdownMenuCheckboxRadio() {
const [columns, setColumns] = useState({ price: true, stock: true, sku: false });
const [sort, setSort] = useState('new');
return (
<div className="flex flex-wrap items-center gap-3">
<DropdownMenu>
<DropdownMenu.Trigger>Ustunlar</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Label>Jadvalda ko'rsatish</DropdownMenu.Label>
{(
[
['price', 'Narx'],
['stock', 'Ombordagi soni'],
['sku', 'Artikul'],
] as const
).map(([key, label]) => (
<DropdownMenu.CheckboxItem
key={key}
checked={columns[key]}
onCheckedChange={(on) => setColumns((c) => ({ ...c, [key]: on }))}
// Menyu yopilmasin — bir nechtasini ketma-ket belgilash uchun
onSelect={(e) => e.preventDefault()}
>
{label}
</DropdownMenu.CheckboxItem>
))}
</DropdownMenu.Content>
</DropdownMenu>
<DropdownMenu>
<DropdownMenu.Trigger>Saralash</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Label>Tartib</DropdownMenu.Label>
<DropdownMenu.RadioGroup value={sort} onValueChange={setSort}>
<DropdownMenu.RadioItem value="new">Avval yangilari</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="cheap">Avval arzonlari</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="expensive">Avval qimmatlari</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="popular" disabled>
Ommabop (tez orada)
</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu>
<span className="text-sm text-text-muted">Saralash: {sort}</span>
</div>
);
}
Submenus
Put a SubTrigger and a SubContent inside Sub. The submenu opens on hover, or with → on the
keyboard.
'use client';
import { DropdownMenu, IconButton } from '@akhatmirzo/ui';
import { Archive, Ellipsis, Pencil, Share2, Trash2 } from 'lucide-react';
export default function DropdownMenuSubmenu() {
return (
<DropdownMenu>
<DropdownMenu.Trigger asChild>
<IconButton variant="outline" aria-label="Ko'proq amallar">
<Ellipsis />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item icon={<Pencil />}>Nomini o'zgartirish</DropdownMenu.Item>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger icon={<Share2 />}>Ulashish</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.Item>Telegram</DropdownMenu.Item>
<DropdownMenu.Item>Email orqali</DropdownMenu.Item>
<DropdownMenu.Item shortcut={['mod', 'L']}>Havolani nusxalash</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger icon={<Archive />}>Ko'chirish</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.Item>Kiruvchi</DropdownMenu.Item>
<DropdownMenu.Item>Arxiv</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
<DropdownMenu.Separator />
<DropdownMenu.Item icon={<Trash2 />} variant="danger">
O'chirish
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
);
}
Profile menu: Label, Group and links
Label and Group split the menu into sections. Give an Item the asChild prop to render an
<a> (or a Next.js Link) as the item — the icon and the shortcut label are placed inside the
link. align="end" lines the panel up with the trigger's right edge.
'use client';
import { Button, DropdownMenu } from '@akhatmirzo/ui';
import { LogOut, Send, Settings, User } from 'lucide-react';
export default function DropdownMenuProfile() {
return (
<DropdownMenu>
<DropdownMenu.Trigger asChild>
<Button variant="ghost" leftIcon={<User />}>
Akmal Karimov
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end" className="w-56">
<DropdownMenu.Label>akmal@misol.uz</DropdownMenu.Label>
<DropdownMenu.Group>
<DropdownMenu.Item icon={<User />} shortcut={['mod', 'P']}>
Profil
</DropdownMenu.Item>
<DropdownMenu.Item icon={<Settings />} shortcut={['mod', ',']}>
Sozlamalar
</DropdownMenu.Item>
<DropdownMenu.Item asChild icon={<Send />}>
<a href="https://github.com/Akhatmirzo/akhatmirzo-toolkit">Loyiha GitHub'da</a>
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item icon={<LogOut />} variant="danger">
Chiqish
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
);
}
<DropdownMenu.Item asChild icon={<Settings />}>
<Link href="/settings">Settings</Link>
</DropdownMenu.Item>shortcut is only a label
shortcut={['mod', 'E']} shows ⌘E in the menu (Ctrl E on Windows) and sets
aria-keyshortcuts, but it does not listen for the key press. Wire up the shortcut itself
with useHotkeys.
States
A disabled item can't be clicked and is skipped by keyboard navigation. The trigger can be
disabled too. Use open + onOpenChange to open the menu from code.
'use client';
import { Button, DropdownMenu } from '@akhatmirzo/ui';
import { Copy, Pencil, Trash2 } from 'lucide-react';
import { useState } from 'react';
export default function DropdownMenuStates() {
const [open, setOpen] = useState(false);
return (
<div className="flex flex-wrap items-center gap-3">
<DropdownMenu>
<DropdownMenu.Trigger>O'chirilgan elementlar</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item icon={<Pencil />}>Tahrirlash</DropdownMenu.Item>
<DropdownMenu.Item icon={<Copy />} disabled>
Nusxa olish (ruxsat yo'q)
</DropdownMenu.Item>
<DropdownMenu.Item icon={<Trash2 />} variant="danger" disabled>
O'chirish (ruxsat yo'q)
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenu.Trigger>Boshqariladigan</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item>Birinchi</DropdownMenu.Item>
<DropdownMenu.Item>Ikkinchi</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
<Button variant="ghost" onClick={() => setOpen(true)}>
Tashqaridan ochish
</Button>
</div>
);
}
Props
DropdownMenu
Accepts the props of the Radix DropdownMenu Root: open, defaultOpen, onOpenChange,
modal (defaults to true) and dir.
DropdownMenu.Content
Also accepts the rest of the Radix Content props (alignOffset, onCloseAutoFocus,
onEscapeKeyDown ...) and <div> attributes.
| Prop | Type | Default | Description |
|---|---|---|---|
side | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | Preferred side |
align | 'start' | 'center' | 'end' | 'start' | Alignment along the trigger |
sideOffset | number | 6 | Gap to the trigger, px |
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) |
DropdownMenu.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 |
textValue | string | — | Text for typeahead when children aren't plain text |
asChild | boolean | false | Renders the child (e.g. an <a>) as the item |
CheckboxItem, RadioGroup, RadioItem
| Prop | Type | Default | Description |
|---|---|---|---|
checked | boolean | 'indeterminate' | — | CheckboxItem: whether it is checked |
onCheckedChange | (checked: boolean) => void | — | CheckboxItem: called on change |
value | string | — | RadioGroup: the selected value; RadioItem: the item's value |
defaultValue | string | — | RadioGroup: initial value |
onValueChange | (value: string) => void | — | RadioGroup: called on change |
shortcut | string[] | — | CheckboxItem and RadioItem: a Kbd on the right |
disabled | boolean | — | CheckboxItem and RadioItem: disables it |
Sub, SubTrigger, SubContent
Sub takes open, defaultOpen and onOpenChange. SubTrigger takes icon and disabled.
SubContent takes sideOffset (default 8), alignOffset (-6), collisionPadding (8),
loop (true) and container.
Data attributes
| Attribute | On | Value |
|---|---|---|
data-state | trigger, panel | open · closed |
data-variant | item | default · danger |
data-highlighted | item | present under the keyboard or mouse |
data-disabled | item | present when disabled |
data-state | Checkbox/RadioItem | checked · unchecked |
data-side | panel | top · right · bottom · left |
Keyboard
| Key | Action |
|---|---|
Enter / Space / ↓ | Opens the menu from the trigger, focusing the first item |
↓ / ↑ | Moves between items (wraps around) |
Home / End | First / last item |
| A letter | Jumps to the next item starting with that letter |
→ / ← | Opens / closes a submenu |
Enter / Space | Selects and closes |
Esc | Closes; focus returns to the trigger |
Accessibility
- Roles. The panel is
role="menu"; items aremenuitem,menuitemcheckboxormenuitemradio; the trigger getsaria-haspopup="menu"andaria-expanded. It is built on Radix DropdownMenu. - Shortcuts. With
shortcut, the item getsaria-keyshortcuts(modbecomesMetaon macOS andControlelsewhere). The visibleKbdis hidden from screen readers. - Icons are
aria-hidden. - Icon triggers. When the trigger is an
IconButton, give it anaria-label("More actions"). - Focus. Opening moves focus into the menu; closing returns it to the trigger.