Skip to main content

Menu

A dropdown menu component built on Base UI Menu primitive with support for items, groups, radio groups, checkboxes, and nested submenus.

Compound Components

The root container that manages menu state.
open
boolean
Controlled open state. When provided, the menu becomes controlled.
defaultOpen
boolean
default:"false"
Uncontrolled default open state.
onOpenChange
(open: boolean) => void
Callback fired when the open state changes.
The trigger element that opens the menu.
className
string
Additional CSS classes to apply.
asChild
boolean
default:"false"
Merge props onto the child element instead of rendering a button.
Portals the menu content to the end of the document body.
keepMounted
boolean
default:"false"
Whether to keep the menu mounted in the DOM when closed.
container
HTMLElement | null
The container element to portal into. Defaults to document.body.
Positions the menu relative to the trigger.
side
'top' | 'right' | 'bottom' | 'left'
default:"'bottom'"
The side of the trigger to position the menu.
align
'start' | 'center' | 'end'
default:"'start'"
The alignment of the menu relative to the trigger.
sideOffset
number
default:"4"
The distance in pixels from the trigger.
alignOffset
number
default:"0"
The offset in pixels along the alignment axis.
collisionPadding
number | { top?: number; right?: number; bottom?: number; left?: number }
default:"8"
The padding in pixels from the viewport edges.
sticky
boolean
default:"true"
Whether the menu should stick to the trigger on scroll.
The menu popup container with list styles.
className
string
Additional CSS classes to apply. Default: Surface overlay background with shadow.
Layout:
  • Min-width: 220px
  • Max-height: 320px (LIST_MAX_HEIGHT)
  • Vertical scrolling when content overflows
Optional arrow pointing to the trigger.
className
string
Additional CSS classes to apply. Default: "fill-surface-overlay"
A clickable menu item.
onSelect
(event: Event) => void
Callback fired when the item is selected.
disabled
boolean
default:"false"
Whether the item is disabled.
closeOnSelect
boolean
default:"true"
Whether to close the menu when this item is selected.
className
string
Additional CSS classes to apply.
Leading content for a menu item (icon, avatar, etc.).
className
string
Additional CSS classes to apply.
Trailing content for a menu item (shortcut, badge, chevron, etc.).
className
string
Additional CSS classes to apply.
Visual separator between menu sections.
className
string
Additional CSS classes to apply.
Groups related menu items.
className
string
Additional CSS classes to apply.
Label for a menu group.
className
string
Additional CSS classes to apply. Default: Small subtle text with padding.
Empty state message when menu has no items.
className
string
Additional CSS classes to apply.
Groups radio items with single selection.
value
string
The currently selected value.
onValueChange
(value: string) => void
Callback fired when the selection changes.
A radio menu item with indicator.
value
string
The value of this radio item.
disabled
boolean
default:"false"
Whether the item is disabled.
className
string
Additional CSS classes to apply.
Indicator: Shows checked circle icon (RiCheckboxCircleFill) when selected. A checkbox menu item with indicator.
checked
boolean | 'indeterminate'
The checked state.
onCheckedChange
(checked: boolean | 'indeterminate') => void
Callback fired when the checked state changes.
disabled
boolean
default:"false"
Whether the item is disabled.
className
string
Additional CSS classes to apply.
Indicator: Shows checked circle icon (RiCheckboxCircleFill) when checked. Root container for a submenu.
open
boolean
Controlled open state.
defaultOpen
boolean
default:"false"
Uncontrolled default open state.
onOpenChange
(open: boolean) => void
Callback fired when the submenu open state changes.
Trigger for opening a submenu.
className
string
Additional CSS classes to apply.

Usage

import { Menu, MenuItem, MenuPrefix, MenuSuffix, MenuSeparator } from '@soft-ui/react/menu'
import { Button } from '@soft-ui/react/button'
import { RiSettings4Line, RiUser3Line, RiLogoutBoxRLine } from '@soft-ui/icons'

function Example() {
  return (
    <Menu.Root>
      <Menu.Trigger asChild>
        <Button>Open Menu</Button>
      </Menu.Trigger>
      <Menu.Portal>
        <Menu.Positioner>
          <Menu.Popup>
            <MenuItem>
              <MenuPrefix><RiUser3Line /></MenuPrefix>
              Profile
              <MenuSuffix>⌘P</MenuSuffix>
            </MenuItem>
            <MenuItem>
              <MenuPrefix><RiSettings4Line /></MenuPrefix>
              Settings
            </MenuItem>
            <MenuSeparator />
            <MenuItem>
              <MenuPrefix><RiLogoutBoxRLine /></MenuPrefix>
              Logout
            </MenuItem>
          </Menu.Popup>
        </Menu.Positioner>
      </Menu.Portal>
    </Menu.Root>
  )
}

Radio Group

<Menu.Popup>
  <Menu.RadioGroup value={theme} onValueChange={setTheme}>
    <Menu.RadioItem value="light">Light</Menu.RadioItem>
    <Menu.RadioItem value="dark">Dark</Menu.RadioItem>
    <Menu.RadioItem value="system">System</Menu.RadioItem>
  </Menu.RadioGroup>
</Menu.Popup>
<Menu.Popup>
  <MenuItem>Account</MenuItem>
  <Menu.SubmenuRoot>
    <Menu.SubmenuTrigger>
      Settings
      <MenuSuffix><RiArrowRightSLine /></MenuSuffix>
    </Menu.SubmenuTrigger>
    <Menu.Portal>
      <Menu.Positioner>
        <Menu.Popup>
          <MenuItem>General</MenuItem>
          <MenuItem>Privacy</MenuItem>
          <MenuItem>Notifications</MenuItem>
        </Menu.Popup>
      </Menu.Positioner>
    </Menu.Portal>
  </Menu.SubmenuRoot>
</Menu.Popup>

Types

export type MenuRootProps = {
  open?: boolean
  defaultOpen?: boolean
  onOpenChange?: (open: boolean) => void
  children?: React.ReactNode
}

export type MenuItemProps = {
  onSelect?: (event: Event) => void
  disabled?: boolean
  closeOnSelect?: boolean
  className?: string
  children?: React.ReactNode
}

Build docs developers (and LLMs) love