Skip to content
ArcadiaDesign
Try it

TSX

Compose the trigger with render: a Button, an IconButton or a PickerTrigger. md is the picker menu (30px rows, 6px padding, full-bleed separators); lg is the action and context menu (32px rows, 8px padding, inset separators); both are radius 12. Section labels are eyebrows. The menu slides 4px toward its trigger as it opens, one highlight glides between its rows (a Glide), and submenus open on hover with no delay.

Icons are one column. Every leading glyph (a line icon, a provider mark, a swatch) sits in the same 16px slot, and once any row in a menu has an icon, the rows without one keep that column, so every label starts on one line. Labels never trail off in dots: the dialog a row opens, or a submenu's chevron, already says more follows.

Which overlay? A Menu lists actions or choices. A Popover holds content you act on, such as a form. A Hover Card previews what a link points to, and a Tooltip names a control in a few words.

Examples

Searchable picker

MenuSearch sits above the scroller and owns typing; the product owns filtering.

Its magnifier stands in the rows' icon column and the query starts on their labels. Each query lights its first match: Enter picks it, and ↓ moves focus onto it, never past it. A key typed on a row goes back to the field.

Opens on whole rows

A menu opens with its checked row in view and the top of its list on a row's top edge, the row above the checked one first, its top edge the list's padding under the field.

18 models, Model O checked

Scrolled under the search field, rows fade into it as far as they went, up to 8px: never sliced by its edge, and never so deep that a row's words dim.

Groups and toggles

Every row closes the menu when you pick it, checkbox rows too, as macOS menus do.

A set of toggles you set together, such as a filter, passes closeOnClick={false} on each row. One setting that modifies another row takes indicator="switch": a switch draws its state, so it never reads as a second pick beside a checked row.

States

Rendered open in place: rest, highlighted, a description, a shortcut, disabled, checked, a submenu trigger and danger, at both sizes. The specimen at the top shows a submenu open.

md: pickers, 30px rows
lg: actions, 32px rows

Trailing content

end takes a dot, a badge or a short text after the label.

220px wide: a shortcut, a dot, a state, a long text

A detail end gives way first: in a menu too narrow for both it steps down by whole words, then goes whole, while the label keeps its words, and the row's tooltip says it all. An end that carries state (endYields={false}) stays: the label steps down by whole words first, never past the form that tells it from the other rows (apart). A row is named for where it leads, never for a state: a dot says something waits there, and the page it opens says what.

Descriptions that give way

A description gives way before the label does.

240px wide: three folders, each said as fits

Pass a FadeText with shorter forms there, such as a folder's last two names, and it fades only when the shortest still doesn't fit.

MenuContent takes where the menu sits and the menu knobs below; each row (MenuItem, MenuRadioItem and the rest) takes the row knobs, each a token of its kind. A colour, a raw length or any other property is a type error.

--menu-min-w
TokenRef<Measure>Default 180px
Narrowest width (180, 220 for lg); the trigger's width when wider.
--menu-max-w
TokenRef<Measure>Default 320px
Widest width (320).
--menu-max-h
TokenRef<Measure>Default 420px
Tallest height (420); the space left beside the trigger when less.
--menu-radius
TokenRef<Radius>Default var(--radius-xl)
Corner radius.
--menu-pad
TokenRef<Space>Default var(--space-1-5)
Padding around the rows (6, 8 for lg).
--menu-gap
TokenRef<Space>Default 0px
Gap between rows.
--menu-item-h
TokenRef<Control>Default 30px
Row height (30, 32 for lg).
--menu-item-py
TokenRef<Space>Default var(--space-1)
Row block padding.
--menu-item-px
TokenRef<Space>Default var(--space-2-5)
Row inline padding on a text side: the content inset of a menu without icons, and every row's end.
--menu-item-px-icon
TokenRef<Space> | Derived
Row start padding in a menu whose rows lead with icons: the icon side, (row height − 16) / 2 (R2).
--menu-item-gap
TokenRef<Space>Default var(--space-2)
Gap between a row's icon, label and end.
--menu-item-radius
TokenRef<Radius>
Row corner radius (the highlight follows it).
--menu-sep-block
TokenRef<Space>Default var(--space-0-75)
Space above and below a separator.

Item corners follow --menu-radius minus --menu-pad; changing the frame or padding keeps the highlight concentric.

--menu-item-fg
PaintDefault var(--text-1)
Label colour.
--menu-item-fg-disabled
PaintDefault var(--text-4)
Label colour when disabled.
--menu-item-icon
PaintDefault var(--icon-2)
Leading icon colour (an element icon such as a provider mark reads it too).
--menu-item-icon-hl
PaintDefault var(--icon-1)
Leading icon colour while the row is highlighted or its submenu is open.
--menu-item-icon-disabled
PaintDefault var(--icon-4)
Leading icon colour when disabled.
--menu-item-fill
PaintDefault var(--fill-4)
The highlight's fill under the pointer (drawn by the menu's Glide).
--menu-item-fill-kbd
PaintDefault var(--fill-3)
The highlight's fill while the keyboard drives it: a step stronger.

Each state reads its own knob (--menu-item-icon-hl, --menu-item-fill-kbd, --menu-item-fg-disabled), so a knob you set never drops a state. An element icon, such as a provider mark, takes the row's icon colour too.

The APG menu pattern: the trigger gets aria-haspopup and aria-expanded, focus moves into the menu and returns to the trigger on close. A row's shortcut is drawn only (aria-hidden, so the row reads "Rename", not "Rename F2") and exposed as aria-keyshortcuts.

↓↑
Move the highlight
EnterSpace
Activate the highlighted row
→
Open a submenu
←
Close a submenu
Esc
Close the menu
A to Z
Jump to a row by its first letters

With a search field

An input may not sit inside role="menu", so a searchable menu moves the roles: the popup is a group labelled by the trigger, the field is a combobox (aria-controls the menu, aria-activedescendant on the lit row, aria-autocomplete="list") and the scroller is the menu.

A to Z
Filter; the first match lights up
Enter
Pick the lit row
↓↑
Move focus onto the lit row (the first press never skips it); then the menu's keys apply
A to Z on a row
Back to the field, the key included
Esc
Close the menu

Menu

open
boolean
Controlled open state.
defaultOpen
booleanDefault false
Initial open state when uncontrolled.
onOpenChange
(open: boolean, details: OpenChangeDetails) => void
Called when the menu opens or closes, with why (details.reason) and the event.
onOpenChangeComplete
(open: boolean) => void
Called once the open or close transition has finished.
modal
booleanDefault true
Lock page scroll and pointer interaction outside while open.

render
RenderProp<{ disabled: boolean }>
Replace the rendered element, e.g. render={<a href="/settings" />}, or a function of props and state.
disabled
boolean
Native disabled: no pointer events, no tooltip, skipped by Tab.
focusableWhenDisabled
boolean
Keep a disabled control focusable and hoverable (aria-disabled), so a tooltip can say why.
nativeButton
boolean
Button semantics, inferred from what renders: a <button> is native, an <a> stays a link, anything else gets role="button" and key handling. false forces button semantics on whatever renders (a link acting as a button).

Rest props go to <button> props.

rootStyle
MenuStyle
Typed StyleX override: where the menu sits (layout) and its --menu-* knobs.
side
"top" | "bottom" | "left" | "right" | "inline-start" | "inline-end"Default "bottom"
align
"start" | "center" | "end"Default "start"
sideOffset
numberDefault 6
alignOffset
number
collisionPadding
number
size
"md" | "lg"Default "md"
md pickers, lg action and context menus.
aria-label
string
The menu's name where its trigger doesn't give one: a context menu ("Chat actions"; default "Context menu"), or with search, what the list under the field holds ("Models", "Projects").
width
"auto" | "trigger" | number
auto (content, 180–320), trigger (exactly the trigger's width) or a pixel width.
search
ReactNode
A <MenuSearch/> row, rendered above the scroller so it never scrolls away.
footer
ReactNode
A band under the rows, never scrolled.
container
PortalContainerDefault document.body
Portal container.
instant
boolean
Skip the open/close transition (re-anchoring from one picker to the next).
finalFocus
FocusTarget
Where focus goes on close. Default: the trigger.
className
string
A product's hook class for its own rules (being retired: new code sets rootStyle).

Rest props go to <div> props.

tone
"neutral" | "danger"Default "neutral"
danger tints label, icon and highlight red (destructive actions).
onClick
(e: MouseEvent<HTMLElement>) => void
Runs the action.
disabled
boolean
Skipped by the keyboard and the pointer, drawn at text-4.
closeOnClick
booleanDefault true
Close the menu after the action (rows and radios: true; checkboxes: false).
label
string
The text typeahead matches (default: the row's text).

Rest props go to <div> props and Pick<CollapsibleHeaderProps, "action" | "details" | "icon" | "end" | "tone" | "loading" | "title" | "className">.

indicator
"check" | "switch"Default "check"
The state at the row's end. check: one of a set picked together. switch: one setting that modifies another row (the model's long context). The row stays the one control.
checked
boolean
Controlled checked state.
defaultChecked
booleanDefault false
Initial checked state when uncontrolled.
onCheckedChange
(checked: boolean, details: { event: Event }) => void
Called with the new state when the row is pressed.
onClick
(e: MouseEvent<HTMLElement>) => void
Runs the action.
disabled
boolean
Skipped by the keyboard and the pointer, drawn at text-4.
closeOnClick
booleanDefault true
Close the menu after the action (rows and radios: true; checkboxes: false).
label
string
The text typeahead matches (default: the row's text).

Rest props go to <div> props and Pick<CollapsibleHeaderProps, "action" | "details" | "icon" | "end" | "tone" | "loading" | "title" | "className">.

label
ReactNode
Section label (an eyebrow), the group's accessible name.
value
unknown
The checked row's value (controlled).
defaultValue
unknown
onValueChange
(value: any, details: { event: Event }) => void
Called with the picked row's value.
disabled
booleanDefault false

Rest props go to <div> props.

valuerequired
unknown
This choice's value.
onClick
(e: MouseEvent<HTMLElement>) => void
Runs the action.
disabled
boolean
Skipped by the keyboard and the pointer, drawn at text-4.
closeOnClick
booleanDefault true
Close the menu after the action (rows and radios: true; checkboxes: false).
label
string
The text typeahead matches (default: the row's text).

Rest props go to <div> props and Pick<CollapsibleHeaderProps, "action" | "details" | "icon" | "end" | "tone" | "loading" | "title" | "className">.

label
ReactNode
Section label (an eyebrow), the group's accessible name.

Rest props go to <div> props.

icon
IconNameDefault "search"
Leading glyph; default search.
autoFocus
boolean
Focus the field when the menu opens.
className
string
Class for the row (the input itself is .ui-menu-search > input).

Rest props go to <input> props.

delay
numberDefault 0
Hover open delay in ms.
onClick
(e: MouseEvent<HTMLElement>) => void
Runs the action.
disabled
boolean
Skipped by the keyboard and the pointer, drawn at text-4.
closeOnClick
booleanDefault false
Close the menu after the action (rows and radios: true; checkboxes: false).
label
string
The text typeahead matches (default: the row's text).

Rest props go to <div> props and Pick<CollapsibleHeaderProps, "action" | "details" | "icon" | "end" | "tone" | "loading" | "title" | "className">.

rootStyle
MenuStyle
Typed StyleX override: where the menu sits (layout) and its --menu-* knobs.
sideOffset
numberDefault 6
alignOffset
number
collisionPadding
number
size
"md" | "lg"Default "md"
md pickers, lg action and context menus.
aria-label
string
The menu's name where its trigger doesn't give one: a context menu ("Chat actions"; default "Context menu"), or with search, what the list under the field holds ("Models", "Projects").
width
"auto" | "trigger" | number
auto (content, 180–320), trigger (exactly the trigger's width) or a pixel width.
search
ReactNode
A <MenuSearch/> row, rendered above the scroller so it never scrolls away.
footer
ReactNode
A band under the rows, never scrolled.
container
PortalContainerDefault document.body
Portal container.
instant
boolean
Skip the open/close transition (re-anchoring from one picker to the next).
finalFocus
FocusTarget
Where focus goes on close. Default: the trigger.
className
string
A product's hook class for its own rules (being retired: new code sets rootStyle).

Rest props go to <div> props.