Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

MenusReact

Menu

A list of actions in a dropdown, opened by a button and navigated with the keyboard.

Rendered by React 19.3.1

Loading React…
import { Menu } from '@hamur/react/menu'

export default function MenuExample() {
  return (
    <Menu.Root>
      <Menu.Trigger className="button">
        Open menu
      </Menu.Trigger>
      <Menu.Portal>
        <Menu.Positioner sideOffset={6}>
          <Menu.Popup className="menu">
            <Menu.Item className="menu-item">
              Rename
            </Menu.Item>
            <Menu.Item className="menu-item">
              Duplicate
            </Menu.Item>
            <Menu.CheckboxItem className="menu-item" defaultChecked>
              Show grid
            </Menu.CheckboxItem>
            <Menu.SubmenuRoot>
              <Menu.SubmenuTrigger className="menu-item">
                Share
              </Menu.SubmenuTrigger>
              <Menu.Portal>
                <Menu.Positioner>
                  <Menu.Popup className="menu">
                    <Menu.Item className="menu-item">
                      Copy link
                    </Menu.Item>
                    <Menu.Item className="menu-item">
                      Email
                    </Menu.Item>
                  </Menu.Popup>
                </Menu.Positioner>
              </Menu.Portal>
            </Menu.SubmenuRoot>
          </Menu.Popup>
        </Menu.Positioner>
      </Menu.Portal>
    </Menu.Root>
  )
}
2,516Tests passing in React · 0 failing
76.3 KiBSize
4.90 msOpen
18.0 msClose
29.0 msOpenMutations
27.0 msCloseMutations
1.35 MiBMemory
productdevbook and 37 upstream authors57 commits · last

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<Menu.Root>
  <Menu.Trigger />
  <Menu.Portal>
    <Menu.Positioner>
      <Menu.Popup>
        <Menu.Item />
        <Menu.CheckboxItem />
        <Menu.SubmenuRoot>
          <Menu.SubmenuTrigger />
        </Menu.SubmenuRoot>
      </Menu.Popup>
    </Menu.Positioner>
  </Menu.Portal>
</Menu.Root>

// Not in this example: Menu.Arrow, Menu.Backdrop, Menu.CheckboxItemIndicator, Menu.Clear, Menu.Empty, Menu.FilterProvider, Menu.Group, Menu.GroupLabel, Menu.Input, Menu.LinkItem, Menu.List, Menu.RadioGroup, Menu.RadioItem, Menu.RadioItemIndicator, Menu.Separator, Menu.Viewport

Examples

Open On Hover

openOnHover and delay open the menu when the pointer rests on the trigger.

MenuOpenOnHover.tsx
import { Menu } from '@hamur/react/menu'

export default function MenuOpenOnHoverExample() {
  return (
    <Menu.Root>
      <Menu.Trigger openOnHover className="button">
        Add to playlist <CaretDownIcon />
      </Menu.Trigger>
      <Menu.Portal>
        <Menu.Positioner sideOffset={8} align="start">
          <Menu.Popup className="menu">
            <Menu.Item className="menu-item">Get Up!</Menu.Item>
            <Menu.Item className="menu-item">Inside Out</Menu.Item>
            <Menu.Item className="menu-item">Night Beats</Menu.Item>
            <Menu.Separator className="menu-separator" />
            <Menu.Item className="menu-item">New playlist…</Menu.Item>
          </Menu.Popup>
        </Menu.Positioner>
      </Menu.Portal>
    </Menu.Root>
  )
}

function CaretDownIcon() {
  return (
    <svg className="caret" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
      <path d="M12 6H4l4 4.5z" />
    </svg>
  )
}

Checkbox Items

CheckboxItem toggles a setting on or off.

MenuCheckboxItems.tsx
import { useState } from 'react'
import { Menu } from '@hamur/react/menu'

export default function MenuCheckboxItemsExample() {
  const [showMinimap, setShowMinimap] = useState(true)
  const [showSearch, setShowSearch] = useState(true)
  const [showSidebar, setShowSidebar] = useState(false)

  return (
    <Menu.Root>
      <Menu.Trigger className="button">
        Workspace <CaretDownIcon />
      </Menu.Trigger>
      <Menu.Portal>
        <Menu.Positioner sideOffset={8} align="start">
          <Menu.Popup className="menu">
            <Menu.CheckboxItem
              className="menu-choice"
              checked={showMinimap}
              onCheckedChange={setShowMinimap}
            >
              <Menu.CheckboxItemIndicator className="menu-choice-indicator">
                <CheckIcon />
              </Menu.CheckboxItemIndicator>
              <span className="menu-choice-text">Minimap</span>
            </Menu.CheckboxItem>
            <Menu.CheckboxItem
              className="menu-choice"
              checked={showSearch}
              onCheckedChange={setShowSearch}
            >
              <Menu.CheckboxItemIndicator className="menu-choice-indicator">
                <CheckIcon />
              </Menu.CheckboxItemIndicator>
              <span className="menu-choice-text">Search</span>
            </Menu.CheckboxItem>
            <Menu.CheckboxItem
              className="menu-choice"
              checked={showSidebar}
              onCheckedChange={setShowSidebar}
            >
              <Menu.CheckboxItemIndicator className="menu-choice-indicator">
                <CheckIcon />
              </Menu.CheckboxItemIndicator>
              <span className="menu-choice-text">Sidebar</span>
            </Menu.CheckboxItem>
          </Menu.Popup>
        </Menu.Positioner>
      </Menu.Portal>
    </Menu.Root>
  )
}

function CaretDownIcon() {
  return (
    <svg className="caret" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
      <path d="M12 6H4l4 4.5z" />
    </svg>
  )
}

function CheckIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor">
      <path d="m2.5 8.5 4 4 7-9" />
    </svg>
  )
}

Radio Items

RadioGroup and RadioItem pick one option of a set.

MenuRadioItems.tsx
import { useState } from 'react'
import { Menu } from '@hamur/react/menu'

export default function MenuRadioItemsExample() {
  const [value, setValue] = useState('date')
  return (
    <Menu.Root>
      <Menu.Trigger className="button">
        Sort <CaretDownIcon />
      </Menu.Trigger>
      <Menu.Portal>
        <Menu.Positioner sideOffset={8} align="start">
          <Menu.Popup className="menu">
            <Menu.RadioGroup value={value} onValueChange={setValue}>
              <Menu.RadioItem className="menu-choice" value="date">
                <Menu.RadioItemIndicator className="menu-choice-indicator">
                  <CheckIcon />
                </Menu.RadioItemIndicator>
                <span className="menu-choice-text">Date</span>
              </Menu.RadioItem>
              <Menu.RadioItem className="menu-choice" value="name">
                <Menu.RadioItemIndicator className="menu-choice-indicator">
                  <CheckIcon />
                </Menu.RadioItemIndicator>
                <span className="menu-choice-text">Name</span>
              </Menu.RadioItem>
              <Menu.RadioItem className="menu-choice" value="type">
                <Menu.RadioItemIndicator className="menu-choice-indicator">
                  <CheckIcon />
                </Menu.RadioItemIndicator>
                <span className="menu-choice-text">Type</span>
              </Menu.RadioItem>
            </Menu.RadioGroup>
          </Menu.Popup>
        </Menu.Positioner>
      </Menu.Portal>
    </Menu.Root>
  )
}

function CaretDownIcon() {
  return (
    <svg className="caret" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
      <path d="M12 6H4l4 4.5z" />
    </svg>
  )
}

function CheckIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor">
      <path d="m2.5 8.5 4 4 7-9" />
    </svg>
  )
}

Group Labels

GroupLabel names a Group or a RadioGroup.

MenuGroupLabels.tsx
import { useState } from 'react'
import { Menu } from '@hamur/react/menu'

export default function MenuGroupLabelsExample() {
  const [value, setValue] = useState('date')
  const [showMinimap, setShowMinimap] = useState(true)
  const [showSearch, setShowSearch] = useState(true)
  const [showSidebar, setShowSidebar] = useState(false)

  return (
    <Menu.Root>
      <Menu.Trigger className="button">
        View <CaretDownIcon />
      </Menu.Trigger>
      <Menu.Portal>
        <Menu.Positioner sideOffset={8} align="start">
          <Menu.Popup className="menu">
            <Menu.RadioGroup value={value} onValueChange={setValue}>
              <Menu.GroupLabel className="menu-label">Sort</Menu.GroupLabel>
              <Menu.RadioItem className="menu-choice" value="date">
                <Menu.RadioItemIndicator className="menu-choice-indicator">
                  <CheckIcon />
                </Menu.RadioItemIndicator>
                <span className="menu-choice-text">Date</span>
              </Menu.RadioItem>
              <Menu.RadioItem className="menu-choice" value="name">
                <Menu.RadioItemIndicator className="menu-choice-indicator">
                  <CheckIcon />
                </Menu.RadioItemIndicator>
                <span className="menu-choice-text">Name</span>
              </Menu.RadioItem>
              <Menu.RadioItem className="menu-choice" value="type">
                <Menu.RadioItemIndicator className="menu-choice-indicator">
                  <CheckIcon />
                </Menu.RadioItemIndicator>
                <span className="menu-choice-text">Type</span>
              </Menu.RadioItem>
            </Menu.RadioGroup>

            <Menu.Separator className="menu-separator" />

            <Menu.Group>
              <Menu.GroupLabel className="menu-label">Workspace</Menu.GroupLabel>
              <Menu.CheckboxItem
                className="menu-choice"
                checked={showMinimap}
                onCheckedChange={setShowMinimap}
              >
                <Menu.CheckboxItemIndicator className="menu-choice-indicator">
                  <CheckIcon />
                </Menu.CheckboxItemIndicator>
                <span className="menu-choice-text">Minimap</span>
              </Menu.CheckboxItem>
              <Menu.CheckboxItem
                className="menu-choice"
                checked={showSearch}
                onCheckedChange={setShowSearch}
              >
                <Menu.CheckboxItemIndicator className="menu-choice-indicator">
                  <CheckIcon />
                </Menu.CheckboxItemIndicator>
                <span className="menu-choice-text">Search</span>
              </Menu.CheckboxItem>
              <Menu.CheckboxItem
                className="menu-choice"
                checked={showSidebar}
                onCheckedChange={setShowSidebar}
              >
                <Menu.CheckboxItemIndicator className="menu-choice-indicator">
                  <CheckIcon />
                </Menu.CheckboxItemIndicator>
                <span className="menu-choice-text">Sidebar</span>
              </Menu.CheckboxItem>
            </Menu.Group>
          </Menu.Popup>
        </Menu.Positioner>
      </Menu.Portal>
    </Menu.Root>
  )
}

function CaretDownIcon() {
  return (
    <svg className="caret" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
      <path d="M12 6H4l4 4.5z" />
    </svg>
  )
}

function CheckIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor">
      <path d="m2.5 8.5 4 4 7-9" />
    </svg>
  )
}

Submenu

SubmenuRoot and SubmenuTrigger open a nested menu.

MenuSubmenu.tsx
import { Menu } from '@hamur/react/menu'

// The submenu sits 4px off its trigger, and lines up with the parent popup's padding.
const offset = ({ side }: { side: string }) => (side === 'top' || side === 'bottom' ? 4 : -4)

export default function MenuSubmenuExample() {
  return (
    <Menu.Root>
      <Menu.Trigger className="button">
        Song <CaretDownIcon />
      </Menu.Trigger>
      <Menu.Portal>
        <Menu.Positioner sideOffset={8} align="start">
          <Menu.Popup className="menu">
            <Menu.Item className="menu-item">Add to Library</Menu.Item>
            <Menu.SubmenuRoot>
              <Menu.SubmenuTrigger className="menu-item">Add to Playlist</Menu.SubmenuTrigger>
              <Menu.Portal>
                <Menu.Positioner sideOffset={offset} alignOffset={offset}>
                  <Menu.Popup className="menu">
                    <Menu.Item className="menu-item">Get Up!</Menu.Item>
                    <Menu.Item className="menu-item">Inside Out</Menu.Item>
                    <Menu.Item className="menu-item">Night Beats</Menu.Item>
                    <Menu.Separator className="menu-separator" />
                    <Menu.Item className="menu-item">New playlist…</Menu.Item>
                  </Menu.Popup>
                </Menu.Positioner>
              </Menu.Portal>
            </Menu.SubmenuRoot>
            <Menu.Separator className="menu-separator" />
            <Menu.Item className="menu-item">Play Next</Menu.Item>
            <Menu.Item className="menu-item">Play Last</Menu.Item>
            <Menu.Separator className="menu-separator" />
            <Menu.Item className="menu-item">Favorite</Menu.Item>
            <Menu.Item className="menu-item">Share</Menu.Item>
          </Menu.Popup>
        </Menu.Positioner>
      </Menu.Portal>
    </Menu.Root>
  )
}

function CaretDownIcon() {
  return (
    <svg className="caret" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
      <path d="M12 6H4l4 4.5z" />
    </svg>
  )
}

Detached Triggers Simple

A trigger outside Root opens it through a handle.

MenuDetachedTriggersSimple.tsx
import { Menu } from '@hamur/react/menu'

const projectMenu = Menu.createHandle()

export default function MenuDetachedTriggersSimpleExample() {
  return (
    <>
      <Menu.Trigger
        className="button button-square"
        handle={projectMenu}
        aria-label="Project actions"
      >
        <EllipsisIcon />
      </Menu.Trigger>
      <Menu.Root handle={projectMenu}>
        <Menu.Portal>
          <Menu.Positioner sideOffset={8} align="start">
            <Menu.Popup className="menu">
              <Menu.Item className="menu-item">Rename</Menu.Item>
              <Menu.Item className="menu-item">Duplicate</Menu.Item>
              <Menu.Item className="menu-item">Move to folder</Menu.Item>
              <Menu.Separator className="menu-separator" />
              <Menu.Item className="menu-item">Archive</Menu.Item>
              <Menu.Item className="menu-item">Delete</Menu.Item>
            </Menu.Popup>
          </Menu.Positioner>
        </Menu.Portal>
      </Menu.Root>
    </>
  )
}

function EllipsisIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
      <circle cx="3" cy="8" r="1" />
      <circle cx="8" cy="8" r="1" />
      <circle cx="13" cy="8" r="1" />
    </svg>
  )
}

Detached Triggers Controlled

open with triggerId lets several triggers share one menu.

MenuDetachedTriggersControlled.tsx
import { useState } from 'react'
import { Menu } from '@hamur/react/menu'

const itemGroups = {
  library: ['Add to library', 'Add to favorites'],
  playback: ['Play', 'Add to queue'],
  share: ['Share', 'Copy link'],
}

const mediaMenu = Menu.createHandle<keyof typeof itemGroups>()

export default function MenuDetachedTriggersControlledExample() {
  const [open, setOpen] = useState(false)
  const [activeTrigger, setActiveTrigger] = useState<string | null>(null)

  const handleOpenChange = (isOpen: boolean, details: Menu.Root.ChangeEventDetails) => {
    setOpen(isOpen)
    if (isOpen) setActiveTrigger(details.trigger?.id ?? null)
  }

  return (
    <>
      <div className="menu-row">
        <Menu.Trigger className="button" handle={mediaMenu} id="menu-trigger-1" payload="library">
          Library
        </Menu.Trigger>
        <Menu.Trigger className="button" handle={mediaMenu} id="menu-trigger-2" payload="playback">
          Playback
        </Menu.Trigger>
        <Menu.Trigger className="button" handle={mediaMenu} id="menu-trigger-3" payload="share">
          Share
        </Menu.Trigger>
        <button
          type="button"
          className="button"
          onClick={() => {
            setActiveTrigger('menu-trigger-2')
            setOpen(true)
          }}
        >
          Open playback (controlled)
        </button>
      </div>
      <Menu.Root
        handle={mediaMenu}
        open={open}
        triggerId={activeTrigger}
        onOpenChange={handleOpenChange}
      >
        {({ payload }) => (
          <Menu.Portal>
            <Menu.Positioner sideOffset={8} align="start">
              <Menu.Popup className="menu">
                {payload &&
                  itemGroups[payload].map((label) => (
                    <Menu.Item key={label} className="menu-item">
                      {label}
                    </Menu.Item>
                  ))}
              </Menu.Popup>
            </Menu.Positioner>
          </Menu.Portal>
        )}
      </Menu.Root>
    </>
  )
}

Arrow

Arrow points the popup at its trigger.

MenuArrow.tsx
import { Menu } from '@hamur/react/menu'

export default function MenuArrowExample() {
  return (
    <Menu.Root>
      <Menu.Trigger className="button">
        Song <CaretDownIcon />
      </Menu.Trigger>
      <Menu.Portal>
        <Menu.Positioner sideOffset={({ side }) => (side === 'top' ? 12 : 8)}>
          <Menu.Popup className="menu">
            <Menu.Arrow className="arrow" />
            <Menu.Item className="menu-item">Add to Library</Menu.Item>
            <Menu.Item className="menu-item">Add to Playlist</Menu.Item>
            <Menu.Separator className="menu-separator" />
            <Menu.Item className="menu-item">Play Next</Menu.Item>
            <Menu.Item className="menu-item">Play Last</Menu.Item>
            <Menu.Separator className="menu-separator" />
            <Menu.Item className="menu-item">Favorite</Menu.Item>
            <Menu.Item className="menu-item">Share</Menu.Item>
          </Menu.Popup>
        </Menu.Positioner>
      </Menu.Portal>
    </Menu.Root>
  )
}

function CaretDownIcon() {
  return (
    <svg className="caret" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
      <path d="M12 6H4l4 4.5z" />
    </svg>
  )
}

Detached Triggers Full

Viewport animates the content when another trigger opens the same menu.

MenuDetachedTriggersFull.tsx
import { Fragment } from 'react'
import { Menu } from '@hamur/react/menu'

const menus = {
  library: {
    heading: 'Library',
    groups: [
      ['Add to library', 'Add to favorites'],
      ['Create playlist', 'Create station'],
    ],
  },
  playback: {
    heading: 'Playback',
    groups: [
      ['Play now', 'Add to queue'],
      ['Play next', 'Play last', 'Sleep timer'],
    ],
  },
  share: {
    heading: 'Share',
    groups: [
      ['Copy link', 'Copy embed code'],
      ['Share to contacts', 'Share to social'],
    ],
  },
}

const mediaMenu = Menu.createHandle<keyof typeof menus>()

export default function MenuDetachedTriggersFullExample() {
  return (
    <div className="menu-row">
      <Menu.Trigger className="button" handle={mediaMenu} payload="library">
        Library
      </Menu.Trigger>
      <Menu.Trigger className="button" handle={mediaMenu} payload="playback">
        Playback
      </Menu.Trigger>
      <Menu.Trigger className="button" handle={mediaMenu} payload="share">
        Share
      </Menu.Trigger>
      <Menu.Root handle={mediaMenu} modal={false}>
        {({ payload }) => (
          <Menu.Portal>
            <Menu.Positioner className="menu-morph-positioner" sideOffset={8} align="start">
              <Menu.Popup className="menu menu-morph">
                <Menu.Viewport className="menu-viewport">
                  {payload &&
                    menus[payload].groups.map((group, index) => (
                      <Fragment key={index}>
                        <Menu.Group>
                          {index === 0 && (
                            <Menu.GroupLabel className="menu-label">
                              {menus[payload].heading}
                            </Menu.GroupLabel>
                          )}
                          {group.map((item) => (
                            <Menu.Item key={item} className="menu-item">
                              {item}
                            </Menu.Item>
                          ))}
                        </Menu.Group>
                        {index < menus[payload].groups.length - 1 && (
                          <Menu.Separator className="menu-separator" />
                        )}
                      </Fragment>
                    ))}
                </Menu.Viewport>
              </Menu.Popup>
            </Menu.Positioner>
          </Menu.Portal>
        )}
      </Menu.Root>
    </div>
  )
}

Filterable Menu

FilterProvider, Input and List narrow the items as the person types.

FilterableMenu.tsx
import { Menu } from '@hamur/react/menu'

export default function FilterableMenuExample() {
  return (
    <Menu.FilterProvider>
      <Menu.Root>
        <Menu.Trigger className="button">Add label</Menu.Trigger>
        <Menu.Portal>
          <Menu.Positioner sideOffset={6}>
            <Menu.Popup className="menu">
              <Menu.Input
                className="input"
                aria-label="Filter labels"
                placeholder="Filter labels"
              />
              <Menu.List>
                <Menu.Item className="menu-item">Bug</Menu.Item>
                <Menu.Item className="menu-item">Documentation</Menu.Item>
                <Menu.Item className="menu-item">Enhancement</Menu.Item>
                <Menu.Item className="menu-item">Question</Menu.Item>
              </Menu.List>
              <Menu.Empty className="muted">No labels found</Menu.Empty>
            </Menu.Popup>
          </Menu.Positioner>
        </Menu.Portal>
      </Menu.Root>
    </Menu.FilterProvider>
  )
}

Filter

A menu of grouped actions with a filter field, and submenus with their own. Groups with no match hide, and the empty state shows when nothing matches.

MenuFilter.tsx
import { Menu } from '@hamur/react/menu'

const sharingOptions = [
  'Email',
  'Messages',
  'AirDrop',
  'Copy link',
  'Invite collaborators',
  'Publish to web',
  'Send a copy',
]
const folderOptions = [
  'Desktop',
  'Documents',
  'Downloads',
  'Projects',
  'Archive',
  'Shared',
  'Trash',
]
const exportOptions = [
  'PDF document',
  'Word document',
  'Plain text',
  'Rich text',
  'Markdown',
  'HTML page',
  'Image',
]
const sortOptions = [
  ['date', 'Date modified'],
  ['name', 'Name'],
  ['size', 'Size'],
]

// The submenus sit 4px off their trigger, and line up with the parent popup's padding.
const offset = ({ side }: { side: string }) => (side === 'top' || side === 'bottom' ? 4 : -4)

export default function MenuFilterExample() {
  return (
    <Menu.FilterProvider>
      <Menu.Root>
        <Menu.Trigger className="button">
          Actions <CaretDownIcon />
        </Menu.Trigger>
        <Menu.Portal>
          <Menu.Positioner sideOffset={8} align="start">
            <Menu.Popup className="menu menu-filter">
              <div className="menu-filter-field">
                <Menu.Input
                  className="menu-filter-input"
                  aria-label="Filter actions"
                  placeholder="e.g. Save"
                />
                <Menu.Clear className="menu-filter-clear">
                  <ClearIcon />
                </Menu.Clear>
              </div>
              <Menu.Empty className="menu-filter-empty">No actions found.</Menu.Empty>
              <Menu.List className="menu-filter-list">
                <Menu.Group className="menu-filter-section">
                  <Menu.GroupLabel className="menu-label">File</Menu.GroupLabel>
                  <Menu.Item className="menu-item">New file</Menu.Item>
                  <Menu.Item className="menu-item">Open file</Menu.Item>
                  <Menu.Item className="menu-item">Save</Menu.Item>
                  <Menu.Item className="menu-item">Save as</Menu.Item>
                  <Menu.Item className="menu-item">Duplicate</Menu.Item>
                  <Menu.Item className="menu-item">Rename</Menu.Item>
                </Menu.Group>
                <Menu.Group className="menu-filter-section">
                  <Menu.Separator className="menu-separator" />
                  <Menu.GroupLabel className="menu-label">Organize</Menu.GroupLabel>
                  <FilterableSubmenu
                    label="Move to folder"
                    inputLabel="Filter folders"
                    placeholder="e.g. Projects"
                    emptyText="No folders found."
                    options={folderOptions}
                  />
                  <Menu.SubmenuRoot>
                    <Menu.SubmenuTrigger className="menu-item">Share</Menu.SubmenuTrigger>
                    <Menu.Portal>
                      <Menu.Positioner sideOffset={offset} alignOffset={offset}>
                        <Menu.Popup className="menu menu-filter">
                          <Menu.List className="menu-filter-list">
                            {sharingOptions.map((option) => (
                              <Menu.Item key={option} className="menu-item">
                                {option}
                              </Menu.Item>
                            ))}
                          </Menu.List>
                        </Menu.Popup>
                      </Menu.Positioner>
                    </Menu.Portal>
                  </Menu.SubmenuRoot>
                  <FilterableSubmenu
                    label="Export"
                    inputLabel="Filter export formats"
                    placeholder="e.g. PDF"
                    emptyText="No export formats found."
                    options={exportOptions}
                  />
                  <Menu.Item className="menu-item">Download a copy</Menu.Item>
                  <Menu.Item className="menu-item">Delete</Menu.Item>
                </Menu.Group>
                <Menu.RadioGroup className="menu-filter-section" defaultValue="date">
                  <Menu.Separator className="menu-separator" />
                  <Menu.GroupLabel className="menu-label">Sort by</Menu.GroupLabel>
                  {sortOptions.map(([value, label]) => (
                    <Menu.RadioItem key={value} className="menu-choice" value={value}>
                      <Menu.RadioItemIndicator className="menu-choice-indicator">
                        <CheckIcon />
                      </Menu.RadioItemIndicator>
                      <span className="menu-choice-text">{label}</span>
                    </Menu.RadioItem>
                  ))}
                </Menu.RadioGroup>
                <Menu.Group className="menu-filter-section">
                  <Menu.Separator className="menu-separator" />
                  <Menu.GroupLabel className="menu-label">View</Menu.GroupLabel>
                  <Menu.CheckboxItem className="menu-choice" defaultChecked>
                    <Menu.CheckboxItemIndicator className="menu-choice-indicator">
                      <CheckIcon />
                    </Menu.CheckboxItemIndicator>
                    <span className="menu-choice-text">Show details</span>
                  </Menu.CheckboxItem>
                  <Menu.CheckboxItem className="menu-choice">
                    <Menu.CheckboxItemIndicator className="menu-choice-indicator">
                      <CheckIcon />
                    </Menu.CheckboxItemIndicator>
                    <span className="menu-choice-text">Show sidebar</span>
                  </Menu.CheckboxItem>
                  <Menu.CheckboxItem className="menu-choice">
                    <Menu.CheckboxItemIndicator className="menu-choice-indicator">
                      <CheckIcon />
                    </Menu.CheckboxItemIndicator>
                    <span className="menu-choice-text">Keep available offline</span>
                  </Menu.CheckboxItem>
                </Menu.Group>
              </Menu.List>
            </Menu.Popup>
          </Menu.Positioner>
        </Menu.Portal>
      </Menu.Root>
    </Menu.FilterProvider>
  )
}

function FilterableSubmenu(props: {
  label: string
  inputLabel: string
  placeholder: string
  emptyText: string
  options: string[]
}) {
  return (
    <Menu.FilterProvider>
      <Menu.SubmenuRoot>
        <Menu.SubmenuTrigger className="menu-item">{props.label}</Menu.SubmenuTrigger>
        <Menu.Portal>
          <Menu.Positioner sideOffset={offset} alignOffset={offset}>
            <Menu.Popup className="menu menu-filter">
              <div className="menu-filter-field">
                <Menu.Input
                  className="menu-filter-input"
                  aria-label={props.inputLabel}
                  placeholder={props.placeholder}
                />
                <Menu.Clear className="menu-filter-clear">
                  <ClearIcon />
                </Menu.Clear>
              </div>
              <Menu.Empty className="menu-filter-empty">{props.emptyText}</Menu.Empty>
              <Menu.List className="menu-filter-list">
                {props.options.map((option) => (
                  <Menu.Item key={option} className="menu-item">
                    {option}
                  </Menu.Item>
                ))}
              </Menu.List>
            </Menu.Popup>
          </Menu.Positioner>
        </Menu.Portal>
      </Menu.SubmenuRoot>
    </Menu.FilterProvider>
  )
}

function CaretDownIcon() {
  return (
    <svg className="caret" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
      <path d="M12 6H4l4 4.5z" />
    </svg>
  )
}

function ClearIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor">
      <path d="m3.5 3.5 9 9m0-9-9 9" />
    </svg>
  )
}

function CheckIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor">
      <path d="m2.5 8.5 4 4 7-9" />
    </svg>
  )
}

API reference

99 props across 25 parts, in React syntax. className, style, render are listed once, under Shared props.

ArrowMenu.Arrow

Displays an element positioned against the menu anchor. Renders a <div>.

Arrow data attributes
Data attributeDescription
data-openPresent when the menu popup is open.
data-closedPresent when the menu popup is closed.
data-sideIndicates which side the popup is positioned relative to the anchor.
data-alignIndicates how the popup is aligned relative to specified side.
data-uncenteredPresent when the menu arrow is uncentered.

BackdropMenu.Backdrop

An overlay displayed beneath the menu popup. Renders a <div>.

Backdrop data attributes
Data attributeDescription
data-openPresent when the menu is open.
data-closedPresent when the menu is closed.
data-starting-stylePresent when the menu begins animating in.
data-ending-stylePresent when the menu is animating out.

CheckboxItemMenu.CheckboxItem

A menu item that toggles a setting on or off. Renders a <div>.

CheckboxItem props
PropTypeDefault
labelOverrides the text used for keyboard text navigation and filtering.string—
defaultCheckedWhether the checkbox item is initially ticked.booleanfalse
checkedWhether the checkbox item is currently ticked.boolean—
onCheckedChangeEvent handler called when the checkbox item is ticked or unticked.((checked: boolean, eventDetails: MenuRootChangeEventDetails) => void)—
onClickThe click handler for the menu item.((event: BaseUIEvent<React.MouseEvent<HTMLDivElement, MouseEvent>>) => void)—
closeOnClickWhether to close the menu when the item is clicked.booleanfalse
nativeButtonWhether the component renders a native <button> element when replacing itbooleanfalse
disabledWhether the component should ignore user interaction.booleanfalse
CheckboxItem data attributes
Data attributeDescription
data-checkedPresent when the menu checkbox item is checked.
data-uncheckedPresent when the menu checkbox item is not checked.
data-disabledPresent when the menu checkbox item is disabled.
data-highlightedPresent when the menu checkbox item is highlighted.

CheckboxItemIndicatorMenu.CheckboxItemIndicator

Indicates whether the checkbox item is ticked. Renders a <span>.

CheckboxItemIndicator props
PropTypeDefault
keepMountedWhether to keep the HTML element in the DOM when the checkbox item is not checked.booleanfalse
CheckboxItemIndicator data attributes
Data attributeDescription
data-checkedPresent when the menu checkbox item is checked.
data-uncheckedPresent when the menu checkbox item is not checked.
data-disabledPresent when the menu checkbox item is disabled.
data-starting-stylePresent when the indicator begins animating in.
data-ending-stylePresent when the indicator is animating out.

ClearMenu.Clear

A button that clears the input text. Requires the menu to be wrapped in Menu.FilterProvider. when the input has text. Renders a <button>.

Clear props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
disabledWhether the component should ignore user interaction.booleanfalse
Clear data attributes
Data attributeDescription
data-disabledPresent when the button is disabled.

EmptyMenu.Empty

A message shown when the menu has no matching items. Requires the menu to be wrapped in Menu.FilterProvider. Renders a <div>.

FilterProviderMenu.FilterProvider

Enables filtering for the menu or submenu it wraps. Add Menu.Input to the popup and place its items in Menu.List. Wrap each searchable submenu in its own provider. Doesn’t render its own element.

FilterProvider props
PropTypeDefault
defaultValueThe uncontrolled filter query when the menu is initially rendered.string—
valueThe filter query.string—
onValueChangeEvent handler called when the filter query changes.((value: string, eventDetails: FilterDropdownRootChangeEventDetails) => void)—
autoHighlightWhether filtering highlights the first matching item automatically.boolean | "always"false
filterFilter function used to match items against the query.((text: string, query: string) => boolean) | null—
localeThe locale the default filter uses for string comparison.Intl.LocalesArgument—
childrenThe <Menu.Root> or <Menu.SubmenuRoot> to make filterable.React.ReactNode—

GroupMenu.Group

Groups related menu items with the corresponding label. Renders a <div>.

Group props
PropTypeDefault
childrenThe content of the component.React.ReactNode—

GroupLabelMenu.GroupLabel

An accessible label that is automatically associated with its parent group. Renders a <div>.

InputMenu.Input

A search field that filters the menu items. Requires the menu to be wrapped in Menu.FilterProvider. Renders an <input>.

Input data attributes
Data attributeDescription
data-highlightedPresent while the input shows its focus ring. Cleared when keyboard navigation highlights an item.

ItemMenu.Item

An individual interactive item in the menu. Renders a <div>.

Item props
PropTypeDefault
labelOverrides the text used for keyboard text navigation and filtering.string—
onClickThe click handler for the menu item.((event: BaseUIEvent<React.MouseEvent<HTMLDivElement, MouseEvent>>) => void)—
closeOnClickWhether to close the menu when the item is clicked.booleantrue
nativeButtonWhether the component renders a native <button> element when replacing itbooleanfalse
disabledWhether the component should ignore user interaction.booleanfalse
Item data attributes
Data attributeDescription
data-highlightedPresent when the menu item is highlighted.
data-disabledPresent when the menu item is disabled.

LinkItemMenu.LinkItem

A link in the menu that can be used to navigate to a different page or section. Renders an <a>.

LinkItem props
PropTypeDefault
labelOverrides the text used for keyboard text navigation and filtering.string—
closeOnClickWhether to close the menu when the item is clicked.booleanfalse
LinkItem data attributes
Data attributeDescription
data-highlightedPresent when the link is highlighted.

ListMenu.List

Groups menu items so other content, such as a filter input, can share the popup. Renders a <div>.

PopupMenu.Popup

A container for the menu items. Renders a <div>.

Popup props
PropTypeDefault
finalFocusDetermines the element to focus when the menu is closed.boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)—
childrenReact.ReactNode—
Popup data attributes
Data attributeDescription
data-openPresent when the menu is open.
data-closedPresent when the menu is closed.
data-starting-stylePresent when the menu begins animating in.
data-ending-stylePresent when the menu is animating out.
data-sideIndicates which side the popup is positioned relative to the anchor.
data-alignIndicates how the popup is aligned relative to specified side.
data-instantPresent if animations should be instant.

PortalMenu.Portal

A portal element that moves the popup to a different part of the DOM. By default, the portal element is appended to <body>. Renders a <div>.

Portal props
PropTypeDefault
containerA parent element to render the portal element into.HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null—
keepMountedWhether to keep the portal mounted in the DOM while the popup is hidden.booleanfalse

PositionerMenu.Positioner

Positions the menu popup against the trigger. Renders a <div>.

Positioner props
PropTypeDefault
disableAnchorTrackingWhether to disable the popup from tracking any layout shift of its positioning anchor.booleanfalse
alignHow to align the popup relative to the specified side.Align'center'
alignOffsetAdditional offset along the alignment axis in pixels.number | OffsetFunction0
sideWhich side of the anchor element to align the popup against.Side'bottom'
sideOffsetDistance between the anchor and the popup in pixels.number | OffsetFunction0
arrowPaddingMinimum distance to maintain between the arrow and the edges of the popup.number5
anchorAn element to position the popup against.Element | VirtualElement | React.RefObject<Element | null> | (() => Element | VirtualElement | null) | null—
collisionAvoidanceDetermines how to handle collisions when positioning the popup.CollisionAvoidance—
collisionBoundaryAn element or a rectangle that delimits the area that the popup is confined to.Boundary'clipping-ancestors'
collisionPaddingAdditional space to maintain from the edge of the collision boundary.Padding5
stickyWhether to maintain the popup in the viewport afterbooleanfalse
positionMethodDetermines which CSS position property to use."absolute" | "fixed"'absolute'
Positioner data attributes
Data attributeDescription
data-openPresent when the menu popup is open.
data-closedPresent when the menu popup is closed.
data-anchor-hiddenPresent when the anchor is hidden.
data-sideIndicates which side the popup is positioned relative to the anchor.
data-alignIndicates how the popup is aligned relative to the specified side.
Positioner CSS variables
CSS variableDescription
--available-widthThe available width between the anchor and the edge of the viewport.
--available-heightThe available height between the anchor and the edge of the viewport.
--anchor-widthThe anchor's width.
--anchor-heightThe anchor's height.
--transform-originThe coordinates that this element is anchored to. Used for animations and transitions.
--positioner-widthThe width of the menu's positioner. It is important to set width to this value when using CSS to animate size changes.
--positioner-heightThe height of the menu's positioner. It is important to set height to this value when using CSS to animate size changes.

RadioGroupMenu.RadioGroup

Groups related radio items. Renders a <div>.

RadioGroup props
PropTypeDefault
defaultValueThe uncontrolled value of the radio item that should be initially selected.any—
valueThe controlled value of the radio item that should be currently selected.any—
onValueChangeFunction called when the selected value changes.((value: any, eventDetails: MenuRootChangeEventDetails) => void)—
disabledWhether the component should ignore user interaction.booleanfalse
childrenThe content of the component.React.ReactNode—

RadioItemMenu.RadioItem

A menu item that works like a radio button in a given group. Renders a <div>.

RadioItem props
PropTypeDefault
labelOverrides the text used for keyboard text navigation and filtering.string—
valuerequiredValue of the radio item.any—
onClickThe click handler for the menu item.((event: BaseUIEvent<React.MouseEvent<HTMLDivElement, MouseEvent>>) => void)—
closeOnClickWhether to close the menu when the item is clicked.booleanfalse
nativeButtonWhether the component renders a native <button> element when replacing itbooleanfalse
disabledWhether the component should ignore user interaction.booleanfalse
RadioItem data attributes
Data attributeDescription
data-checkedPresent when the menu radio item is selected.
data-uncheckedPresent when the menu radio item is not selected.
data-disabledPresent when the menu radio item is disabled.
data-highlightedPresent when the menu radio item is highlighted.

RadioItemIndicatorMenu.RadioItemIndicator

Indicates whether the radio item is selected. Renders a <span>.

RadioItemIndicator props
PropTypeDefault
keepMountedWhether to keep the HTML element in the DOM when the radio item is inactive.booleanfalse
RadioItemIndicator data attributes
Data attributeDescription
data-checkedPresent when the menu radio item is selected.
data-uncheckedPresent when the menu radio item is not selected.
data-disabledPresent when the menu radio item is disabled.
data-starting-stylePresent when the radio indicator begins animating in.
data-ending-stylePresent when the radio indicator is animating out.

RootMenu.Root

Groups all parts of the menu. Doesn’t render its own element.

Root props
PropTypeDefault
defaultOpenWhether the menu is initially open.booleanfalse
openWhether the menu is currently open.boolean—
onOpenChangeEvent handler called when the menu is opened or closed.((open: boolean, eventDetails: MenuRootChangeEventDetails) => void)—
highlightItemOnHoverWhether moving the pointer over items should highlight them.booleantrue
actionsRefA ref to imperative actions.React.RefObject<MenuRootActions | null>—
closeParentOnEscWhen in a submenu, determines whether pressing the Escape keybooleanfalse
defaultTriggerIdID of the trigger that the menu is associated with.string | null—
handleA handle to associate the menu with a trigger.MenuHandle<Payload>—
loopFocusWhether to loop keyboard focus back to the first itembooleantrue
modalDetermines if the menu enters a modal state when open.booleantrue
onItemHighlightedCallback fired when an item is highlighted or unhighlighted.((highlightedItem: HTMLElement | undefined, eventDetails: MenuRootHighlightEventDetails) => void)—
onOpenChangeCompleteEvent handler called after any animations complete when the menu is opened or closed.((open: boolean) => void)—
triggerIdID of the trigger that the menu is associated with.string | null—
disabledWhether the component should ignore user interaction.booleanfalse
orientationThe visual orientation of the menu.MenuRootOrientation'vertical'
childrenThe content of the menu.React.ReactNode | PayloadChildRenderFunction<Payload>—

SeparatorMenu.Separator

A separator element accessible to screen readers. Renders a <div>.

Separator props
PropTypeDefault
orientationThe orientation of the separator.Orientation'horizontal'
Separator data attributes
Data attributeDescription
data-orientationIndicates the orientation of the separator.

SubmenuRootMenu.SubmenuRoot

Groups all parts of a submenu. Doesn’t render its own element.

SubmenuRoot props
PropTypeDefault
defaultOpenWhether the menu is initially open.booleanfalse
openWhether the menu is currently open.boolean—
onOpenChangeEvent handler called when the menu is opened or closed.((open: boolean, eventDetails: MenuRootChangeEventDetails) => void)—
highlightItemOnHoverWhether moving the pointer over items should highlight them.booleantrue
actionsRefA ref to imperative actions.React.RefObject<MenuRootActions | null>—
closeParentOnEscWhen in a submenu, determines whether pressing the Escape keybooleanfalse
loopFocusWhether to loop keyboard focus back to the first itembooleantrue
onItemHighlightedCallback fired when an item is highlighted or unhighlighted.((highlightedItem: HTMLElement | undefined, eventDetails: MenuRootHighlightEventDetails) => void)—
onOpenChangeCompleteEvent handler called after any animations complete when the menu is opened or closed.((open: boolean) => void)—
disabledWhether the component should ignore user interaction.booleanfalse
orientationThe visual orientation of the menu.MenuRootOrientation'vertical'
childrenThe content of the submenu.React.ReactNode—

SubmenuTriggerMenu.SubmenuTrigger

A menu item that opens a submenu. Renders a <div>.

SubmenuTrigger props
PropTypeDefault
labelOverrides the text used for keyboard text navigation and filtering.string—
onClick((event: BaseUIEvent<React.MouseEvent<HTMLDivElement, MouseEvent>>) => void)—
nativeButtonWhether the component renders a native <button> element when replacing itbooleanfalse
disabledWhether the component should ignore user interaction.booleanfalse
openOnHoverWhether the menu should also open when the trigger is hovered.booleantrue
delayHow long to wait before the menu may be opened on hover.number100
closeDelayHow long to wait before closing the menu that was opened on hover.number0
SubmenuTrigger data attributes
Data attributeDescription
data-popup-openPresent when the corresponding submenu is open.
data-highlightedPresent when the submenu trigger is highlighted.
data-disabledPresent when the submenu trigger is disabled.

TriggerMenu.Trigger

A button that opens the menu. Renders a <button>.

Trigger props
PropTypeDefault
handleA handle to associate the trigger with a menu.MenuHandle<Payload>—
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
payloadA payload to pass to the menu when it is opened.Payload—
disabledWhether the component should ignore user interaction.booleanfalse
openOnHoverWhether the menu should also open when the trigger is hovered.boolean—
delayHow long to wait before the menu may be opened on hover.number100
closeDelayHow long to wait before closing the menu that was opened on hover.number0
childrenReact.ReactNode—
Trigger data attributes
Data attributeDescription
data-popup-openPresent when the corresponding menu is open.
data-pressedPresent when the trigger is pressed.
data-disabledPresent when the trigger is disabled.

ViewportMenu.Viewport

A viewport for displaying content transitions. This component is only required if one popup can be opened by multiple triggers, its content changes based on the trigger, and switching between them is animated. Renders a <div>.

Viewport props
PropTypeDefault
childrenThe content to render inside the transition container.React.ReactNode—
Viewport data attributes
Data attributeDescription
data-currentApplied to the direct child of the viewport when no transitions are present or the new content when it's entering.
data-previousApplied to the direct child of the viewport that contains the exiting content when transitions are present.
data-activation-directionIndicates the direction from which the popup was activated. This can be used to create directional animations based on how the popup was triggered. Contains space-separated values for both horizontal and vertical axes.
data-transitioningIndicates that the viewport is currently transitioning between old and new content.
data-instantPresent if animations should be instant.
Viewport CSS variables
CSS variableDescription
--popup-widthThe width of the parent popup. This variable is placed on the 'previous' container and stores the width of the popup when the previous content was rendered. It can be used to freeze the dimensions of the popup when animating between different content.
--popup-heightThe height of the parent popup. This variable is placed on the 'previous' container and stores the height of the popup when the previous content was rendered. It can be used to freeze the dimensions of the popup when animating between different content.

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
EnterSpaceArrowDownArrowUpOpens the menuTrigger
ArrowDownOpens the menu and focuses the first itemTrigger
ArrowUpOpens the menu and focuses the last itemTrigger
ArrowDownArrowUpMoves to the next or previous itemPopup
HomeEndMoves to the first or last itemPopup
Moves to the next item whose text, or label, starts with what was typedItem
EnterActivates the highlighted item and closes the menuItem
SpaceChecks or unchecks a checkbox itemCheckboxItem
EnterChecks or unchecks a checkbox itemCheckboxItem
EnterSpaceSelects a radio itemRadioItem
ArrowRightOpens a submenu and focuses its first item (ArrowLeft in right-to-left)SubmenuTrigger
ArrowLeftCloses a submenu and returns focus to its triggerSubmenuTrigger
EscapeCloses the menu and keeps focus on the triggerPopup
EscapeIn a submenu, closes only the submenu unless closeParentOnEsc is onSubmenuRoot
TabCloses the menu and moves focus to the element after the triggerPopup

Accessibility

What the parts set
PartSetsSo that
Triggeraria-haspopup="menu", aria-expanded, aria-controlsa screen reader says the button opens a menu and whether it is open
Popuprole="menu"the popup is announced as a menu
Itemrole="menuitem"each action is read as a menu item
CheckboxItemrole="menuitemcheckbox", aria-checkeda setting is read with its on or off state
RadioItemrole="menuitemradio", aria-checkedthe chosen option of a set is read
Grouprole="group", aria-labelledby to its GroupLabelrelated items are read under their label
SubmenuTriggeraria-controls to the submenua screen reader knows the item opens another menu
Separatorrole="separator"groups are divided for screen readers too
CheckboxItemIndicatoraria-hidden="true"the tick is decoration; the state is on the item
Arrowaria-hidden="true"the decorative arrow is not read

Focus order

  1. Opening with a key moves focus to the first item, or the last with ArrowUp.
  2. Arrow keys move focus between items; only one item is a tab stop.
  3. Escape closes the menu and returns focus to Trigger; Tab closes it and moves on.