Menu
Actions on the current object, sort orders and view options, in an anchored list opened by a Button.
import {
Menu,
MenuCheckboxItem,
MenuGroup,
MenuGroupLabel,
MenuItem,
MenuPopup,
MenuRadioGroup,
MenuRadioItem,
MenuSeparator,
MenuSubmenu,
MenuSubmenuTrigger,
MenuTrigger,
} from '@fairgarden-private/design/components/Menu'
The trigger is a Button: an outline or text Button with a title-case label ("Sort By"), or an icon-only "…". The popup renders in a portal as a white scope that follows the page mode, whatever the trigger's ground, with a --border-size-2 --primary12 edge. It sits 8 px from the trigger, aligned to its start edge, 200 to 320 px wide and at least as wide as the trigger. Rows are 44 px at every width, with a leading slot, a sentence-case label and an optional shortcut. It opens instantly, or with a clip reveal from the trigger side, and is hidden in print; the state it sets prints as a caption.
Use a menu for actions on the current object (more, sort, export, share, row actions), not for page navigation or form values.
Action menu
Action menu
Highlight, by pointer or keyboard, is a soft fill plus a --ds-stroke-3 start-edge bar; the bar alone survives forced colors. A destructive item shows the danger glyph ◆ and names the verb and object, so color is never the only cue. A disabled item is muted, takes no highlight and says why where it can. Submenus fly out to the end side.
No action yet.
'use client'
import * as React from 'react'
import {
Menu,
MenuGroup,
MenuGroupLabel,
MenuItem,
MenuPopup,
MenuSeparator,
MenuSubmenu,
MenuSubmenuTrigger,
MenuTrigger,
} from '@fairgarden-private/design/components/Menu'
import styles from './actions.module.css'
export function MenuActions() {
const [last, setLast] = React.useState('No action yet.')
const run = (label: string) => () => setLast(`${label}.`)
return (
<div className={styles.stack}>
<div className={styles.row}>
<span className={styles.object}>Field note, 12 May</span>
<Menu>
<MenuTrigger iconOnly icon="more_horiz">
Note Actions
</MenuTrigger>
<MenuPopup>
<MenuItem icon="download" shortcut="⌘ D" onClick={run('Downloaded the note')}>
Download note
</MenuItem>
<MenuItem icon="search" shortcut="⌘ F" onClick={run('Searched similar notes')}>
Find similar notes
</MenuItem>
<MenuSubmenu>
<MenuSubmenuTrigger icon="arrow_forward">Move to</MenuSubmenuTrigger>
<MenuPopup>
<MenuItem onClick={run('Moved to Spring count')}>Spring count</MenuItem>
<MenuItem onClick={run('Moved to Meadow survey')}>Meadow survey</MenuItem>
<MenuItem disabled>Archive (needs 1 photo)</MenuItem>
</MenuPopup>
</MenuSubmenu>
<MenuSeparator />
<MenuGroup>
<MenuGroupLabel>Danger zone</MenuGroupLabel>
<MenuItem destructive onClick={run('Deleted the note')}>
Delete note
</MenuItem>
</MenuGroup>
</MenuPopup>
</Menu>
</div>
<p className={styles.status} aria-live="polite">
{last}
</p>
</div>
)
}
Sort and options, on forest
Sort and options, on forest
A checked radio item shows a leading ●, and a checked checkbox item a leading ✓, in --primary12 with the label at --font-weight-6: no fill and no bar. The sort trigger shows the current value. On the forest ground the trigger takes forest's roles, while the popup keeps its white scope.
'use client'
import * as React from 'react'
import { Ground } from '@fairgarden-private/design/components/Ground'
import {
Menu,
MenuCheckboxItem,
MenuGroup,
MenuGroupLabel,
MenuPopup,
MenuRadioGroup,
MenuRadioItem,
MenuSeparator,
MenuTrigger,
} from '@fairgarden-private/design/components/Menu'
import styles from './options.module.css'
const orders = {
distance: 'Distance',
climb: 'Climb',
name: 'Name',
} as const
type Order = keyof typeof orders
export function MenuOptions() {
const [order, setOrder] = React.useState<Order>('distance')
const [showClosed, setShowClosed] = React.useState(false)
const [dogFriendly, setDogFriendly] = React.useState(true)
return (
<Ground preset="forest" kind="field" className={styles.face}>
<p className={styles.caption}>
The trigger follows the forest ground; the popup stays a white scope in the page mode.
</p>
<Menu>
<MenuTrigger variant="outline" icon="expand_more" iconPosition="end">
{`Sort: ${orders[order]}`}
</MenuTrigger>
<MenuPopup>
<MenuGroup>
<MenuGroupLabel>Sort by</MenuGroupLabel>
<MenuRadioGroup value={order} onValueChange={(value: Order) => setOrder(value)}>
{(Object.keys(orders) as Order[]).map((key) => (
<MenuRadioItem key={key} value={key}>
{orders[key]}
</MenuRadioItem>
))}
</MenuRadioGroup>
</MenuGroup>
<MenuSeparator />
<MenuGroup>
<MenuGroupLabel>Show</MenuGroupLabel>
<MenuCheckboxItem checked={showClosed} onCheckedChange={setShowClosed}>
Closed trails
</MenuCheckboxItem>
<MenuCheckboxItem checked={dogFriendly} onCheckedChange={setDogFriendly}>
Dog-friendly only
</MenuCheckboxItem>
</MenuGroup>
</MenuPopup>
</Menu>
<p className={styles.caption} aria-live="polite">
{`View: sorted by ${orders[order].toLowerCase()}`}
{showClosed ? ' · closed trails shown' : ''}
{dogFriendly ? ' · dog-friendly only' : ''}
</p>
</Ground>
)
}
API Reference
Menu, MenuTrigger and MenuPopup
Groups the parts of a menu (Base UI Menu.Root).
Opens the menu. Renders a Button (§9.2): an outline or text Button
with a title-case label (“Sort By”, “Export”) [D160, D165], or an
icon-only more_horiz “…". A sort trigger shows its current value.
| Prop | Type | Description |
|---|---|---|
butted | | Butts the button against an adjacent field on its |
handle | | Associates a detached trigger with a Menu created by |
icon | | One optional functional glyph (§6.10), inline tier, FILL 0. |
iconOnly | |
|
iconPosition | | Which side of the label the glyph sits on (a |
onMedia | | Only with |
onPress | | Called on click, after |
payload | | A payload handed to the Menu’s children function when this trigger opens it. |
primary | | Primary Radix scale, from the primary roster: the outline edge, labels and focus ring. Never defaulted; omitted, it inherits the scope [D133]. |
secondary | | Secondary Radix scale: the |
size | | Fixed height: |
variant | |
|
children | | The label: verb plus object, authored in title case [D160]. |
The popup, rendered through its Base UI Portal as a nested white
scope: --primary1 face, --border-size-2 --primary12 edge, --ds-radius-8,
--size-px-1 padding, 200 px to --size-px-14 wide and at least the
trigger’s width, --size-px-2 from the trigger, aligned to its start
edge. It opens instantly or with a clip reveal from the trigger side.
Inside a MenuSubmenu it flies out to the end side.
| Prop | Type | Description |
|---|---|---|
primary | | Primary Radix scale inside the popup’s |
secondary | | Secondary Radix scale inside the popup. It drives nothing but a destructive item’s fallback. |
container | | The element the portal renders into. Default: |
align | | Alignment to the trigger. Default |
alignOffset | | Offset along the alignment axis in px. |
side | | Side of the trigger. Default |
sideOffset | | Distance from the trigger in px. Default 8 ( |
collisionPadding | | Clearance from the viewport edge in px before the popup shifts or flips. Default 16. |
keepMounted | | Keeps the portal mounted while closed. |
MenuItem
An action row (Base UI Menu.Item): --ds-size-hit tall at every
breakpoint, a --size-px-5 leading slot, the label in type-body-ui
sentence case, and an optional trailing shortcut. Highlight (pointer or
keyboard) is the --primary4 fill plus the start-edge bar; activation
closes the menu.
| Prop | Type | Description |
|---|---|---|
destructive | | Shows the danger glyph (◆ with its inner ×) in the leading slot; the
label, which names the verb and object (“Delete Note”), stays
--primary12, so color is never the only cue. Default |
icon | | A leading Material Symbols glyph at the inline tier (§6.10). |
secondary | | The item’s secondary scale. Omitted, a destructive item takes the danger scale [D129]. |
shortcut | | A trailing keyboard shortcut, in |
MenuCheckboxItem, MenuRadioGroup and MenuRadioItem
An option that toggles (Base UI Menu.CheckboxItem). Checked shows a leading ✓ in --primary12 and sets the label at --font-weight-6, with no fill and no bar [D145]. It stays open on activation.
Groups radio items into one choice, such as a sort order (Base UI Menu.RadioGroup).
One choice in a MenuRadioGroup (Base UI Menu.RadioItem). Checked shows a
leading ● in --primary12 (the FILL 1 circle, always with the weight
change) and the label at --font-weight-6; no fill and no bar [D145, D166].
MenuGroup, MenuGroupLabel and MenuSeparator
Groups related items under a MenuGroupLabel (Base UI Menu.Group).
A group’s label, in type-label --role-muted; not focusable (Base UI Menu.GroupLabel).
A full-width --border-size-1 --role-rule between groups, --size-px-1 above and below.
MenuSubmenu and MenuSubmenuTrigger
Groups a submenu’s trigger and popup (Base UI Menu.SubmenuRoot). The nested MenuPopup flies out to the end side at --size-px-2 and flips at the viewport edge.
The row that opens a submenu (Base UI Menu.SubmenuTrigger), with a
trailing chevron_right at the inline tier. It stays highlighted while
its submenu is open.
| Prop | Type | Description |
|---|---|---|
icon | | A leading Material Symbols glyph at the inline tier (§6.10). |
Additional types
menu
MenuCheckboxItemProps
MenuGroupLabelProps
MenuGroupProps
menuItem
MenuItemProps
MenuPopupProps
MenuProps
MenuRadioGroupProps
MenuRadioItemProps
MenuSeparatorProps
MenuSubmenuProps
MenuSubmenuTriggerProps
MenuTriggerProps
Specification: DESIGN-SYSTEM.md §9.7 (menu), §9.1 (overlay panels) and §1.11.4 (list-option states).