Menu
A list of actions opened from a trigger.
import { Menu } from '@aksornproject/aksara-ui'Basic menu
Menu is a namespace of parts with the Base UI Menu names and nesting. Use it to run actions. To pick a value, use Select.
Menu.Positioner places the panel below the trigger with the right edges aligned and an 8px gap. Menu.Popup is the 280px panel. Group items under a Menu.GroupLabel inside a Menu.Group, and divide them with Menu.Separator.
<Menu.Root>
<Menu.Trigger>Actions</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.Group>
<Menu.GroupLabel>Homework</Menu.GroupLabel>
<Menu.Item>Edit</Menu.Item>
<Menu.Item>Duplicate</Menu.Item>
<Menu.Item endContent="⌘S">Share</Menu.Item>
</Menu.Group>
<Menu.Separator />
<Menu.Item destructive>Delete</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>Triggers
Menu.Trigger draws a labelled trigger with a chevron. size is default (44px tall) or small (36px). The label truncates at 240px. The chevron points down while closed and up while open. chevron={false} hides it.
iconOnly makes a square trigger with no label or chevron. It shows the dots icon unless a leadingIcon or children replace it. Give it an aria-label. disabled stops the trigger opening the menu.
<ActionsMenu>Actions</ActionsMenu>
<ActionsMenu size="small">Actions</ActionsMenu>
<ActionsMenu iconOnly aria-label="More actions" />
<ActionsMenu size="small" iconOnly aria-label="More actions" />
<ActionsMenu leadingIcon={<IconInterfaceEssentialSetting />}>
Settings
</ActionsMenu>
<ActionsMenu chevron={false}>No chevron</ActionsMenu>
<ActionsMenu disabled>Disabled</ActionsMenu>Items
Children are the label. It wraps and the row grows. description adds a second line. leadingIcon is a 16px slot. endContent holds a shortcut or similar and truncates at 60px. destructive is for delete actions only.
A disabled item stays focusable so keyboard users can find it, but it does not run. Menu.LinkItem renders the same row as an <a>.
leadingIcon and endContent take an element, a function or anything else. A single element becomes the slot and gets the slot classes and the state as data-* attributes. It must forward its props to the DOM. A function (props, state) => element gets the slot props to spread and the item state, { destructive, disabled }. Anything else renders inside a slot span. The trigger leadingIcon works the same way with the trigger state, { open, disabled, size, iconOnly }.
<Menu.Root>
<Menu.Trigger>Assignment</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.Item leadingIcon={<IconInterfaceEssentialCopy />}>
Duplicate
</Menu.Item>
<Menu.Item
leadingIcon={<IconInterfaceEssentialShare />}
description="Anyone with the link can view"
>
Share
</Menu.Item>
<Menu.Item endContent="⌘E">Edit</Menu.Item>
<Menu.Item disabled>Archive</Menu.Item>
<Menu.LinkItem
href="/docs/components/dialog"
leadingIcon={<IconInterfaceEssentialAttachmentLink />}
>
Open the Dialog page
</Menu.LinkItem>
<Menu.Separator />
<Menu.Item
destructive
leadingIcon={<IconInterfaceEssentialTrashDeleteBin2 />}
>
Delete
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>Checkbox and radio items
Menu.CheckboxItem draws the checkbox slot. Menu.RadioItem goes inside a Menu.RadioGroup and shows a tick when checked. Checked rows of both get the selection fill.
The radio tick sits at the end. indicator="start" moves it to the start. Pass closeOnClick to close the menu after a pick.
<Menu.Root>
<Menu.Trigger>View</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.CheckboxItem checked={grid} onCheckedChange={setGrid}>
Show grid
</Menu.CheckboxItem>
<Menu.Separator />
<Menu.RadioGroup value={sort} onValueChange={setSort}>
<Menu.GroupLabel>Sort by</Menu.GroupLabel>
<Menu.RadioItem value="newest" closeOnClick>
Newest
</Menu.RadioItem>
<Menu.RadioItem value="oldest" closeOnClick>
Oldest
</Menu.RadioItem>
<Menu.RadioItem value="name" indicator="start" closeOnClick>
Name
</Menu.RadioItem>
</Menu.RadioGroup>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>Submenu
Menu.SubmenuRoot wraps a Menu.SubmenuTrigger and the Menu.Portal it opens. The trigger row shows a chevron. The nested Menu.Positioner opens the panel beside the row with the first rows lined up.
<Menu.Root>
<Menu.Trigger>Lesson</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner align="start">
<Menu.Popup>
<Menu.Item>Edit</Menu.Item>
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger>Move to</Menu.SubmenuTrigger>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.Item>Unit 1</Menu.Item>
<Menu.Item>Unit 2</Menu.Item>
<Menu.Item>Archive</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.SubmenuRoot>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>Search
Wrap the menu in Menu.FilterProvider and put Menu.Search in the popup. Without the provider, Base UI throws an error that says so. Base UI hides the items that do not match. Pass filter={null} to the provider to filter the items yourself.
The search row stays above the list while the list scrolls. Put Menu.Search directly in Menu.Popup or inside a fragment. The arrow keys move the highlight while the field keeps focus. The clear button shows while the field has text.
<Menu.FilterProvider>
<Menu.Root>
<Menu.Trigger>Assign to class</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.Search />
{CLASSES.map((name) => (
<Menu.Item key={name}>{name}</Menu.Item>
))}
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
</Menu.FilterProvider>Scrolling
maxHeight on Menu.Popup caps the item list, in px or any CSS length. The list scrolls natively past it with an 8px gutter. Without it, the panel hugs its items up to the space the viewport has.
<Menu.Root>
<Menu.Trigger>Jump to week</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup maxHeight={240}>
{WEEKS.map((week) => (
<Menu.Item key={week}>{week}</Menu.Item>
))}
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>Custom trigger
render replaces the built-in trigger with your element, such as a library Button. The trigger then adds only the behaviour and ARIA, with no classes or content. render cannot be combined with size, iconOnly, leadingIcon or chevron.
Here the Button keeps its focus ring while the menu is open through forceFocusVisible.
<Menu.Root open={open} onOpenChange={setOpen}>
<Menu.Trigger
render={(props) => (
<Button
{...props}
iconOnly
color="brand"
variant="tertiary-accent"
size="sm"
aria-label="Grade actions"
forceFocusVisible={open}
>
<IconInterfaceEssentialDots />
</Button>
)}
/>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.Item
destructive
leadingIcon={<IconInterfaceEssentialTrashDeleteBin2 />}
>
Delete grade
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>Accessibility
- The trigger is a native button with
aria-haspopup="menu"andaria-expanded. Give an icon-only trigger anaria-label. - Items are
menuitem,menuitemcheckboxormenuitemradio. - Arrow keys, Home, End and typeahead move between items. Enter and Space run them, and Escape closes.
Menu.Searchis a searchbox named "Search". It sits outside themenurole. Its clear button stays out of the tab order and the accessibility tree.- The menu is modal by default. Pass
modal={false}toMenu.Rootto keep the page scrollable and clickable while it is open.
API
Menu.Root
Prop
Type
Menu.Trigger
Prop
Type
Menu.Portal
Prop
Type
Menu.Positioner
side defaults to bottom, align to end and sideOffset to 8. Inside a Menu.SubmenuRoot, side defaults to inline-end, align to start and alignOffset to -4.
Prop
Type
Menu.Popup
Prop
Type
Menu.Item
Prop
Type
Menu.LinkItem
Prop
Type
Menu.CheckboxItem
Prop
Type
Menu.RadioGroup
Prop
Type
Menu.RadioItem
Prop
Type
Menu.Group
Prop
Type
Menu.GroupLabel
Prop
Type
Menu.Separator
Prop
Type
Menu.SubmenuRoot
Menu.SubmenuTrigger
Menu.Search
placeholder defaults to "Search..." and aria-label to "Search". The ref and className reach the input.
Prop
Type
Menu.FilterProvider
Prop
Type