Components
Menus
DropdownMenu and ContextMenu are two triggers for one menu language: actions and options attached to a trigger, or to a target. Radix owns focus, typeahead, submenus and Escape; LUC owns the surface.
Examples
import {
Button,
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
IconButton,
} from "@luc/ui";Dropdown menu
Icons lead, shortcuts trail, and a destructive action is a negative label rather than a filled red row — nothing implies the click already happened.
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary" trailingIcon={<RiArrowDownSLine />}>Run actions</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuLabel>This run</DropdownMenuLabel>
<DropdownMenuItem onSelect={openReport}>
<RiTableLine aria-hidden="true" />
Open report
<DropdownMenuShortcut>⌘O</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem tone="danger">Delete run</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>Choices inside a menu
Checkbox and radio items are for options you are toggling in place, not for picking a value a form should own — that is Select or Combobox.
Sorted by sharpe.
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Columns</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuLabel>Visible columns</DropdownMenuLabel>
<DropdownMenuCheckboxItem
checked={columns.volume}
onCheckedChange={(next) => setColumns((prev) => ({ ...prev, volume: Boolean(next) }))}
>
Volume
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem checked={columns.drawdown} onCheckedChange={setDrawdown}>
Drawdown
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>Context menu on a row
The same actions, attached to the object itself. Right click, or press Shift+F10 / the context-menu key with the row focused. Every row is a fair target — the menu is not a replacement for a visible action when one action dominates.
| Run | Status | Actions |
|---|---|---|
| btc-sma-2026-09 | Complete | |
| eth-momentum-2026-09 | Running | |
| macro-regime-2026-08 | Failed |
<ContextMenu>
<ContextMenuTrigger asChild>
<tr className="border-b border-line-subtle">
<td className="px-3 py-2">Run 128</td>
<td className="px-3 py-2">Momentum</td>
</tr>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Open</ContextMenuItem>
<ContextMenuItem>Duplicate</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem tone="danger">Delete</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>Which control?
Four components look similar in a screenshot and behave completely differently. Pick by the question being answered.
Select
Choose one value from a known, short list. Reads like a form field and reports a value.
Combobox
Search, then choose. Use when the list is long, remote or free-text filtered.
DropdownMenu
Run an action, or toggle options, attached to a trigger. Nothing is submitted.
ContextMenu
The same actions, attached to the object the pointer is on.
CommandMenu
Global navigation and search across the application.
API
| Prop | Type | Default | Notes |
|---|---|---|---|
DropdownMenu* | Radix DropdownMenu parts | — | Root, Trigger, Content, Item, CheckboxItem, RadioGroup, RadioItem, Sub/SubTrigger/SubContent, Label, Separator. |
ContextMenu* | Radix ContextMenu parts | — | Identical surface and row rhythm; the trigger is a target instead of a button. |
shortcut | ReactNode | — | Trailing keyboard hint on an item. Purely presentational. |
variant | "default" | "destructive" | "default" | Destructive is a negative label, not a filled red row. |
inset | boolean | false | Aligns a row without a leading indicator with rows that have one. |
sideOffset | number | 6 | Distance from the trigger; menu sections use 4. |
Rules
Do
- Keep the first action visible on the row and put the rest in the menu.
- Name the trigger for what it acts on — “Actions for btc-sma-2026-09”.
- Group with labels and separators once there are more than about five items.
Don't
- Use a menu to pick a form value.
- Hide the only route to a primary action inside a context menu.
- Colour a whole row red — the label carries the tone.
Usage
import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuSeparator } from "@luc/ui/context-menu";<ContextMenu>
<ContextMenuTrigger>Right-click this row</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem shortcut="⌘C">Copy id</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem variant="danger">Archive</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>Guidelines
Do
- Use it for actions on the object the pointer is over, and keep the same actions reachable from a visible control for touch and keyboard readers.
- Write the `shortcut` as the key chord the app actually binds; it is a hint beside the label, not a binding the menu installs.
Don't
- Put the only path to an action in a context menu; there is no right-click on a phone and no discoverable affordance on a page.
Accessibility
The trigger opens on the keyboard's context-menu key as well as on right-click, and the content is a menu: Arrow keys move, Escape closes, and focus returns to the trigger. Items are announced with their shortcut as a description.
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from "@luc/ui/dropdown-menu";<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary">Actions</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="danger">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>DropdownMenu
Do
- Use it for a short list of commands that all act on the page's current object, and set `align` so the panel opens toward the available space.
- Use `variant="danger"` for a destructive item; it is the colour and weight the reader has learned to read as destructive.
Don't
- Put more than a handful of items in a dropdown; a menu the reader has to scroll is a list that wants its own surface.
Accessibility
The trigger is a menu button with `aria-haspopup`; the content is a `menu` with roving focus. Arrow keys move, Escape closes, typeahead jumps to an item, and focus returns to the trigger on close.
Related