|Kit

Menu

A dropdown of actions on Ark UI's Menu — composable parts plus a typesafe data-driven form, with sections, submenus, checkbox / radio items, and a context-menu trigger.

Menu is the kit's dropdown of actions, a styled layer over Ark UI's Menu. Ark owns the open state, positioning, roving focus, type-ahead, and the role="menu" / menuitem ARIA wiring; the kit adds the portaled, animated surface and Mantine-style item props. It ships two APIs from one import — compose the parts for full control, or pass an items array for the common case.

import { Menu } from '@42-central/ui-react/menu';

<Menu.Root>
  <Menu.Trigger asChild>
    <Button>Open</Button>
  </Menu.Trigger>
  <Menu.Content>
    <Menu.Item startSection={<User />} onClick={openProfile}>Profile</Menu.Item>
    <Menu.Divider />
    <Menu.Item color="red" startSection={<Trash />} onClick={remove}>Delete</Menu.Item>
  </Menu.Content>
</Menu.Root>

Playground

Menu

Loading preview

Props

Code

<Menu
  position="bottom-start"
  offset={8}
  closeOnSelect
  loopFocus={false}
/>

Basic usage

Menu.Item takes startSection / endSection (RTL-aware, like Mantine's left/right sections) and a color for destructive actions. Menu.Label heads a section and Menu.Divider separates them. The dropdown is portaled (escapes overflow: hidden ancestors) and animated.

Code

<Menu.Root>
  <Menu.Trigger asChild>
    <Button endSlot={<ChevronDown />}>Account</Button>
  </Menu.Trigger>
  <Menu.Content>
    <Menu.Label>Application</Menu.Label>
    <Menu.Item startSection={<User />}>Profile</Menu.Item>
    <Menu.Item startSection={<Settings />} endSection={<span>⌘S</span>}>Settings</Menu.Item>
    <Menu.Item startSection={<Users />}>Team</Menu.Item>
    <Menu.Divider />
    <Menu.Item startSection={<LogOut />}>Log out</Menu.Item>
    <Menu.Item color="red" startSection={<Trash2 />}>Delete account</Menu.Item>
  </Menu.Content>
</Menu.Root>

Data-driven

For simple action menus, describe the whole thing as a data array — built on the same parts, the way Autocomplete is built on Combobox. Each item's onClick fires on activation (click and keyboard); a bare string is shorthand for { label, value }, { type: 'divider' } is a separator, and { group, items } is a section. A group takes an optional decorative prefix (the code-themed // leader, off by default; hidden from screen readers and the clipboard). The component is generic over const data, so an item's explicit value narrows the optional top-level onSelect(value).

Nothing selected yet

Code

<Menu
  data={[
    { label: 'Profile', value: 'profile', startSection: <User /> },
    { label: 'Settings', value: 'settings', startSection: <Settings /> },
    { type: 'divider' },
    { group: 'Danger zone', prefix: '// ', items: [
      { label: 'Delete', value: 'delete', color: 'red', startSection: <Trash2 /> },
    ]},
  ]}
  onSelect={(value) => setLast(value)} // value: 'profile' | 'settings' | 'delete'
>
  <Button endSlot={<ChevronDown />}>Actions</Button>
</Menu>

An item that navigates should be a real link, so middle-click, "open in new tab", the URL on hover and the link role all work. Pass your router's link component as the item's only child with asChild: the item becomes that link, and startSection, endSection, color and the highlight still apply. Keyboard selection (Enter) clicks the link, so client-side routing keeps working. Every item part composes the same way: Menu.CheckboxItem, Menu.RadioItem, Menu.SubTrigger and Menu.ItemGroupLabel included.

Code

import { Link as RouterLink } from 'your-router';

<Menu.Content>
  <Menu.Item asChild startSection={<BookOpen />}>
    <RouterLink href="/docs/breadcrumb">Breadcrumb docs</RouterLink>
  </Menu.Item>
  <Menu.Item asChild startSection={<BookOpen />}>
    <RouterLink href="/docs/alert">Alert docs</RouterLink>
  </Menu.Item>
  <Menu.Divider />
  <Menu.Item asChild startSection={<Link />} endSection={<ExternalLink />}>
    <a href="https://ark-ui.com/docs/components/menu" target="_blank" rel="noreferrer">
      Ark UI Menu
    </a>
  </Menu.Item>
</Menu.Content>

Nest a Menu.Sub and open it from a Menu.SubTrigger; its dropdown is a Menu.SubContent (same surface as Menu.Content). In the data-driven form, an item with both a label and its own items becomes a submenu automatically.

Code

<Menu.Content>
  <Menu.Item startSection={<Copy />}>Copy</Menu.Item>
  <Menu.Item startSection={<Scissors />}>Cut</Menu.Item>
  <Menu.Divider />
  <Menu.Sub>
    <Menu.SubTrigger startSection={<Share2 />}>Share</Menu.SubTrigger>
    <Menu.SubContent>
      <Menu.Item startSection={<Link />}>Copy link</Menu.Item>
      <Menu.Item startSection={<Mail />}>Email</Menu.Item>
    </Menu.SubContent>
  </Menu.Sub>
</Menu.Content>

Checkbox & radio

Menu.CheckboxItem and Menu.RadioGroup / Menu.RadioItem carry selection state. Set closeOnSelect={false} on Menu.Root to keep the dropdown open while toggling several options.

Code

<Menu.Root closeOnSelect={false}>
  <Menu.Trigger asChild><Button endSlot={<ChevronDown />}>View</Button></Menu.Trigger>
  <Menu.Content>
    <Menu.Label>Editor</Menu.Label>
    <Menu.CheckboxItem value="wrap" checked={wrap} onCheckedChange={setWrap}>Word wrap</Menu.CheckboxItem>
    <Menu.CheckboxItem value="minimap" checked={minimap} onCheckedChange={setMinimap}>Minimap</Menu.CheckboxItem>
    <Menu.Divider />
    <Menu.Label>Theme</Menu.Label>
    <Menu.RadioGroup value={theme} onChange={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.Content>
</Menu.Root>

Context menu

Swap Menu.Trigger for Menu.ContextTrigger to open on right-click (and long-press on touch).

Code

<Menu.Root>
  <Menu.ContextTrigger asChild>
    <div>Right-click here</div>
  </Menu.ContextTrigger>
  <Menu.Content>
    <Menu.Item startSection={<Copy />}>Copy</Menu.Item>
    <Menu.Item startSection={<Scissors />}>Cut</Menu.Item>
    <Menu.Divider />
    <Menu.Item color="red" startSection={<Trash2 />}>Delete</Menu.Item>
  </Menu.Content>
</Menu.Root>

Anatomy

  • Menu.Root — wraps Ark's Menu.Root; owns open state + positioning.
  • Menu.Trigger — the button that opens the menu; use asChild to wrap your own element. Menu.ContextTrigger opens on right-click instead.
  • Menu.Content — the portaled, animated dropdown.
  • Menu.Item — an action row, with startSection / endSection / color.
  • Menu.Label / Menu.Divider — a section heading and a separator. For ARIA-grouped sections use Menu.ItemGroup + Menu.ItemGroupLabel. Both headings accept an optional decorative prefix (e.g. "// ", off by default).
  • Menu.CheckboxItem, Menu.RadioGroup / Menu.RadioItem — stateful selection rows.
  • Menu.Sub / Menu.SubTrigger / Menu.SubContent — a nested submenu.

API

Menu.Root props (beyond Ark's Menu.Root):

Prop

Type

Every rendered part — Menu.Content, Menu.Item, Menu.CheckboxItem, Menu.RadioItem, Menu.SubTrigger, Menu.Label, Menu.ItemGroupLabel, Menu.Divider — also takes its own testId prop, rendered as data-testid on that part. See the Testing guide.

Menu.Item props (beyond Ark's Menu.Item):

Prop

Type

<Menu data> (data-driven) props:

Prop

Type

Each entry also accepts an optional testId, defaulting to its resolved value (explicit or auto-generated) — see the Testing guide.

Changelog

v0.18.0
  • Menu.Item, CheckboxItem, RadioItem, SubTrigger and ItemGroupLabel render as your router link under asChild, with their sections, indicator and highlight

    #288
  • Pressing Enter on a link item navigates client-side instead of reloading the page

    #288
  • A Menu built from a non-literal items array types onSelect's value as string instead of any

    #287
v0.16.0
  • An outside click no longer dismisses the parent while a nested overlay is open

    #251
v0.15.0
  • The trigger no longer shows the keyboard focus ring after a mouse selection

    #235
  • Size startSection icons so labels share a column

    #230
v0.9.0
  • Opened inside a Modal or Drawer, an overlay portals into it, so it always paints above its host

    #135
v0.3.1
  • Borders and dividers on floating panels match the panel's own surface

    #37
v0.3.0
  • items → data on RadioGroup, ChoiceCardGroup, SegmentGroup and Menu

    #34
  • onClose fires alongside onOpenChange, as on Modal

    #34
  • Panels and highlights use translucent washes instead of opaque grays

    #35
v0.1.0
  • New Menu: composable parts or a data-driven wrapper, with submenus, sections and RTL-aware placement

    #4

On this page