Dropdown Menu
Open a menu of actions or options from a button.
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, MenuPopup } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use Dropdown Menu for actions such as open, rename or delete, and for view settings. Use Select to choose a value, or Command to search a long list. You can also offer the same actions in a Context Menu, while keeping the dropdown as a visible way to reach them.
import { useState } from 'react'
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger } from '@youj/react'
import { MoreHorizontalIcon, PencilIcon, TrashIcon } from '@youj/icons'
export function ProjectActions() {
const [open, setOpen] = useState(false)
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger render={<Button variant="ghost" size="icon-sm" aria-label="Project actions" />}>
<MoreHorizontalIcon active={open} />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={rename}>
<PencilIcon />
Rename
<DropdownMenuShortcut>⌘R</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="danger" onClick={remove}>
<TrashIcon />
Delete project
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}MoreHorizontalIcon is a state icon: driven by the menu's open state, its dots become a close mark while the menu is open and return when it closes. Run an item's action from onClick. It fires for pointer and keyboard alike, then the menu closes. A leading icon is muted until its row is highlighted. Youj icons also play their choreography then, because a menu item is their trigger. DropdownMenuShortcut shows a row's shortcut, trailing and muted. It's a label only, so bind the keys yourself.
Description
A project card with an actions menu of items, groups, shortcuts, a submenu and a danger row.
- Items run an action.
variant="danger"tints the row red, for deleting, revoking and leaving.disabledkeeps a row visible but out of reach. - Groups and labels.
DropdownMenuGroupgathers related rows, and aDropdownMenuLabelinside a group names it for screen readers. Outside a group, a label is a plain caption. - Separators divide groups. The highlight glides across them rather than blinking out.
- Submenus.
DropdownMenuSubholds aDropdownMenuSubTriggerrow and aDropdownMenuSubContent, which opens beside the row with its first item level with it. - Inset.
inseton a row or label indents it to line up with rows that have an icon.
A menu with checkbox settings and a radio group, which stays open while they change.
DropdownMenuCheckboxItem toggles a setting and DropdownMenuRadioGroup with DropdownMenuRadioItem picks one option. Both keep the menu open, so several settings can change in one visit. A check draws itself in when a row turns on.
An account menu built from an avatar trigger, a header and a Theme submenu.
The trigger can be any button: here an avatar and a name. The header is plain content, and the Theme submenu holds a radio group. The inset row lines up with the icon rows above it.
Code
dropdown-menu-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propscontainer, presentation, inset… (4)
Props
DropdownMenuContent
| Prop | Type | |
|---|---|---|
container | HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null>Where the portal renders. Default: document.body. | – |
presentation | OverlayPresentation'auto''sheet''popover'
default 'auto' | 'auto' |
DropdownMenuItem
| Prop | Type | |
|---|---|---|
inset | booleanIndents the label to line up with rows that have an icon. | – |
variant | 'default' | 'danger'
default 'default' | 'default' |
onSelect | 'onSelect is the DOM text-selection event and never fires on a menu item: use onClick' | – |
Other props pass through: extends MenuPrimitive.Item.Props. | ||
DropdownMenu takes Base UI's root props (open, defaultOpen, onOpenChange, modal, which is on by default), and DropdownMenuTrigger takes the trigger's (openOnHover, delay, render). DropdownMenuCheckboxItem takes checked, defaultChecked and onCheckedChange, and DropdownMenuRadioGroup takes value, defaultValue and onValueChange. Rows and labels take inset, rows take disabled, and closeOnClick changes whether choosing an item closes the menu (on for items, off for checkbox and radio items). DropdownMenuSubContent takes the same placement props as the content, opening on inline-end by default.
MotionOpen, close, highlight… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
Openscale .96 → 1 and fade, out of the trigger (yj-pop) | duration.fastease.enter140ms · cubic-bezier(0.16, 1, 0.3, 1) | 140mscubic-bezier(0.16, 1, 0.3, 1) | |
| Close | duration.instantease.exit80ms · cubic-bezier(0.4, 0, 1, 1) | 80mscubic-bezier(0.4, 0, 1, 1) | |
Highlightone SlidingIndicator per popup glides between rows | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
| Highlight across a separatorholds its row this long before it lets go | duration.fast140ms | 140ms | |
| Destructive washred tint on the highlighted row | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| Checkdraws in when a row turns on | duration.slowease.emphasized340ms · cubic-bezier(0.6, 0, 0.1, 1) | 340mscubic-bezier(0.6, 0, 0.1, 1) | |
Keyboard open, Escape closeBase UI sets data-instant | no token 0ms | 0ms |
Reduced motion
The menu appears at once, the highlight jumps from row to row, and checks appear already drawn.
Rows paint nothing themselves on hover. A single highlight per popup follows Base UI's highlighted row, so moving quickly down a menu reads as one traveling surface, and a mid-glide change of direction retargets it with its velocity. In forced-colors mode the fill is dropped and the highlighted row gets an outline.
Reduced motion
The menu opens and closes at once and the highlight moves without gliding. Checks are drawn as soon as a row turns on. Rows, keyboard behavior and focus handling don't change.
AccessibilityKeyboard, ARIA, focus
Accessibility
- The trigger has
aria-haspopup="menu"andaria-expanded, and the popup is amenu. Rows aremenuitem,menuitemcheckboxormenuitemradio, witharia-checkedwhere they hold state. - A label inside a group names the group. Radio items sit in a
group, so the choice is announced with its name. - Focus moves into the menu when it opens and returns to the trigger when it closes. While open, the menu is modal: the page doesn't scroll and outside presses only close it.
- The popup itself is a tab stop, so a menu long enough to scroll is reachable from the keyboard.
Tabcloses the menu.
| Key | Action |
|---|---|
Enter / Space / ↓ | Opens the menu from the trigger and focuses the first row. |
↑ | Opens the menu from the trigger and focuses the last row. |
↑ / ↓ | Moves between rows. |
Home / End | Jumps to the first or last row. |
| Letters | Jump to the next row that starts with them (typeahead). |
Enter / Space | Runs the row, or toggles a checkbox or radio row. |
→ / ← | Opens a submenu on its first row, or closes it back onto its row. Mirrored in right-to-left layouts. |
Escape | Closes the menu and returns focus to the trigger. |