Navigation

Menu

Dropdown menu of actions opened from a button: items with icons and shortcuts, checkbox and radio options, submenus, a search header and a metadata footer.

Installation

Install the package (React 19):

terminal
npm install @krackedui/motion

Import the styles once, in your app entry or root layout:

/src/main.tsx
import '@krackedui/motion/styles.css';

Use it anywhere inside your app's <Theme> (see Installation):

/src/MenuExample.tsx
import { useState } from 'react';
import { IconButton, Menu } from '@krackedui/motion';

export function PageMenu() {
  const [wrap, setWrap] = useState(true);
  const [sort, setSort] = useState('edited');
  return (
    <Menu.Root>
      <Menu.Trigger asChild>
        <IconButton icon="more" label="Page actions" />
      </Menu.Trigger>
      <Menu.Content width={240} align="end">
        <Menu.Item icon="link" shortcut="⌘L">Copy link</Menu.Item>
        <Menu.Item icon="copy" shortcut="⌘D">Duplicate</Menu.Item>
        <Menu.Sub>
          <Menu.SubTrigger icon="share">Share to</Menu.SubTrigger>
          <Menu.SubContent width={180}>
            <Menu.Item>Email</Menu.Item>
            <Menu.Item>Chat</Menu.Item>
          </Menu.SubContent>
        </Menu.Sub>
        <Menu.Separator />
        <Menu.CheckboxItem checked={wrap} onCheckedChange={setWrap}>Wrap text</Menu.CheckboxItem>
        <Menu.Separator />
        <Menu.Label>Sort by</Menu.Label>
        <Menu.RadioGroup value={sort} onValueChange={setSort}>
          <Menu.RadioItem value="edited">Last edited</Menu.RadioItem>
          <Menu.RadioItem value="created">Created</Menu.RadioItem>
          <Menu.RadioItem value="title">Title</Menu.RadioItem>
        </Menu.RadioGroup>
        <Menu.Separator />
        <Menu.Item icon="trash" destructive>Delete</Menu.Item>
      </Menu.Content>
    </Menu.Root>
  );
}

Examples

Checkbox, radio and submenu

Options keep their state outside the menu; submenus open on → or hover.

header / footer slots sit outside the item list. ToggleItem is the switch row and keeps the menu open.

Static surface

The same look without behavior, for custom pickers and previews.

Copy link⌘L
Duplicate⌘D
Lock page
Move to Trash
130 words
Edited today at 4:55 PM

Composition

anatomy.tsx
<Menu.Root>
  <Menu.Trigger asChild>…</Menu.Trigger>
  <Menu.Content header={<Menu.Search />} footer={<Menu.Footer />}>
    <Menu.Label />
    <Menu.Item />
    <Menu.CheckboxItem />
    <Menu.RadioGroup><Menu.RadioItem /></Menu.RadioGroup>
    <Menu.Sub><Menu.SubTrigger /><Menu.SubContent>…</Menu.SubContent></Menu.Sub>
    <Menu.Separator />
  </Menu.Content>
</Menu.Root>
  • Every part also has a flat alias for Server Components: MenuRoot, MenuItem, MenuCheckboxItem…
  • MenuSurface, MenuList, MenuRow, MenuLabel, MenuSeparator are the same look without behavior, for pickers that manage focus themselves.

API Reference

Holds the open state.

PropTypeDefault
open
boolean
defaultOpen
boolean
false
onOpenChange
(open: boolean) => void
modal
boolean
true

The button that opens the menu.

PropTypeDefault
asChild
boolean
false

The floating menu (portalled to body).

PropTypeDefault
width
number
header
ReactNode
footer
ReactNode
side
"top""right""bottom""left"
"bottom"
sideOffset
number
4
align
"start""center""end"
"start"
alignOffset
number
0
avoidCollisions
boolean
true
collisionPadding
number | Partial<Record<Side, number>>
8
onEscapeKeyDown
(event: KeyboardEvent) => void
onPointerDownOutside
(event: PointerDownOutsideEvent) => void
onCloseAutoFocus
(event: Event) => void
forceMount
true
loop
boolean
false

An action row.

PropTypeDefault
icon
IconName | ReactNode
children
ReactNode
description
ReactNode
caption
ReactNode
shortcut
ReactNode
submenu
boolean
false
checked
boolean
false
toggle
boolean
trailing
ReactNode
tone
"default""danger""accent""muted"
"default"
destructive
boolean
false
disabled
boolean
false
onSelect
(event: Event) => void
textValue
string

An on/off option (menuitemcheckbox) with a trailing checkmark when checked.

PropTypeDefault
icon
IconName | ReactNode
children
ReactNode
description
ReactNode
caption
ReactNode
shortcut
ReactNode
checked
boolean | "indeterminate"
onCheckedChange
(checked: boolean) => void
disabled
boolean
false
onSelect
(event: Event) => void
textValue
string

Groups RadioItems; exactly one is selected.

PropTypeDefault
value
string
onValueChange
(value: string) => void

One option of a RadioGroup (menuitemradio), checkmark when selected.

PropTypeDefault
icon
IconName | ReactNode
children
ReactNode
description
ReactNode
caption
ReactNode
shortcut
ReactNode
value
string
disabled
boolean
false
onSelect
(event: Event) => void
textValue
string

Motion extra: an on/off row drawn with a switch. Keeps the menu open when toggled.

PropTypeDefault
icon
IconName | ReactNode
children
ReactNode
description
ReactNode
caption
ReactNode
checked
boolean
onCheckedChange
(checked: boolean) => void
disabled
boolean
false
onSelect
(event: Event) => void
textValue
string

Wraps a submenu (SubTrigger + SubContent).

PropTypeDefault
open
boolean
defaultOpen
boolean
false
onOpenChange
(open: boolean) => void

Row that opens a submenu (→ or Enter opens, ← closes). Draws the chevron itself.

PropTypeDefault
icon
IconName | ReactNode
children
ReactNode
description
ReactNode
caption
ReactNode
disabled
boolean
false
textValue
string

The submenu surface (portalled).

PropTypeDefault
width
number
sideOffset
number
2
collisionPadding
number
8

Section label, hairline separator and a grouping wrapper.

PropTypeDefault
className
string

Search: filter field for the header slot (its accessible name defaults to the placeholder). Footer: grey metadata lines for the footer slot.

PropTypeDefault
noIcon
boolean
false
wrapperClassName
string
lines
ReactNode[]
separator
boolean
true

Static menu card.

PropTypeDefault
width
number

Static section (role listbox; give it an aria-label) with 4px padding. A <div>: all its props pass through.

PropTypeDefault
separator
boolean
false

Static row (role option; a toggle row adds aria-checked). A <div>: pass tabIndex={0} to make an actionable row a Tab stop (Enter / Space click it).

PropTypeDefault
icon
IconName | ReactNode
children
ReactNode
description
ReactNode
caption
ReactNode
shortcut
ReactNode
submenu
boolean
false
checked
boolean
false
toggle
boolean
trailing
ReactNode
tone
"default""danger""accent""muted"
"default"
highlighted
boolean
false
onToggle
(next: boolean) => void

The purely visual switch drawn by toggle rows.

PropTypeDefault
checked
boolean
false

Search field for the top of a static menu or Menu.Content header: an <input type="search"> (all input props pass through) in a 12px-padded wrapper. Its accessible name defaults to the placeholder.

PropTypeDefault
noIcon
boolean
false
wrapperClassName
string

SearchField

The same 28px search input without the wrapper padding, for toolbars and custom pickers. All <input> props pass through.

PropTypeDefault
noIcon
boolean
false

Grey metadata lines at the bottom of a menu ("130 words", "Edited today"). A <div>: all its props pass through.

PropTypeDefault
lines
ReactNode[]
separator
boolean
true

Static section label (12px, secondary), hairline separator (role="separator"), and the accent "New" pill for rows. Plain elements: all HTML props pass through.

PropTypeDefault
className
string

Accessibility

  • Follows the WAI-ARIA menu button pattern: the trigger has aria-haspopup="menu" and aria-expanded.
  • Enter, Space or ↓ on the trigger opens the menu and focuses the first item; ↑ opens it on the last item.
  • ↑/↓ move between items (wrapping), Home/End jump to the ends, typing a letter jumps to the matching item.
  • Enter or Space activates the item; → opens a submenu, ← or Escape closes it. Escape closes the menu and returns focus to the trigger.
  • Checkbox and radio items expose menuitemcheckbox / menuitemradio with aria-checked.
  • Static parts: MenuList is a listbox (give it an aria-label) of MenuRow options; a toggle row is an option with aria-checked. Rows are not Tab stops by default and a click keeps focus in the field driving the list (aria-activedescendant pickers); pass tabIndex={0} to make an actionable row a Tab stop that Enter / Space activate.
  • Content is collision-aware: it flips and shifts to stay 8px inside the viewport and scrolls when taller than the space left.
  • Rows are 28px tall, above the 24px minimum target. Opens and closes with the shared motion layer (styles/motion.css); with prefers-reduced-motion: reduce it only fades.

Usage

  • Use for a short list of actions or view options attached to a button ("…", "Sort", "Actions").
  • Use CheckboxItem for independent on/off options and RadioGroup + RadioItem for one-of-many choices.
  • Mark destructive actions with destructive and put them last, after a separator.
  • Don't use a menu to pick a form value: use Select or Combobox, which work with forms and labels.
  • Don't put long or searchable lists in a menu: use CommandPalette.
  • Shortcut hints are display-only: register the keyboard shortcut yourself.
© 2026 Motion. Internal documentation.