Skip to main content

Menu

  • Component overview: Sidebar navigation menu for file management and category navigation. Comprises Menu (container), MenuItem (nav item), and MenuGroup (collapsible group). Supports both declarative items config and composition API.
  • Size baseline: MenuItem min-height 40px, padding 8px, font 14px/20px, icon 20×20px; MenuGroup header min-height 40px.
  • Implementation note: Pure design-layer component — Menu renders as <nav>, MenuGroup is built on Radix Collapsible with open/close animation. Selection state managed via value / defaultValue / onValueChange.
  • Figma spec

Basic Usage

Configure menu items declaratively via items; selection is handled by defaultValue.

Result
Loading...
Live Editor
render(
  <div style={{ width: 220 }}>
    <Menu
      defaultValue="files"
      items={[
        { value: 'files', label: 'All files', count: '(572)' },
        { value: 'unfiled', label: 'Unfiled', count: '(203)' },
        { value: 'trash', label: 'Trash', count: '(82)' },
      ]}
    />
  </div>,
)

Composition API

Use MenuItem children directly for more flexibility. The value prop on each item enables automatic selection via Menu's defaultValue.

Result
Loading...
Live Editor
render(
  <div style={{ width: 220 }}>
    <Menu header="Files" defaultValue="files">
      <MenuItem value="files" label="All files" count="(572)" />
      <MenuItem value="unfiled" label="Unfiled" count="(203)" />
      <MenuItem value="trash" label="Trash" count="(82)" />
    </Menu>
  </div>,
)

With Icons

Add a leading icon to each item via the icon prop.

Result
Loading...
Live Editor
const SearchIcon = () => (
  <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
    <circle cx="9" cy="9" r="5.5" stroke="currentColor" strokeWidth="1.2" />
    <path d="M13 13L16 16" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
  </svg>
)

const HomeIcon = () => (
  <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M3 10L10 4L17 10M5 9V16H8V12H12V16H15V9" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
  </svg>
)

render(
  <div style={{ width: 220 }}>
    <Menu defaultValue="home">
      <MenuItem value="search" icon={<SearchIcon />} label="Search" />
      <MenuItem value="home" icon={<HomeIcon />} label="Home" />
    </Menu>
  </div>,
)

Controlled Selection

Use value + onValueChange for fully controlled selection.

Result
Loading...
Live Editor
const ControlledExample = () => {
  const [value, setValue] = useState('files')

  return (
    <div style={{ width: 220 }}>
      <Menu value={value} onValueChange={setValue}>
        <MenuItem value="files" label="All files" count="(572)" />
        <MenuItem value="unfiled" label="Unfiled" count="(203)" />
        <MenuItem value="trash" label="Trash" count="(82)" />
      </Menu>
      <p style={{ fontSize: 12, color: '#757575', marginTop: 8, paddingLeft: 8 }}>
        Selected: {value}
      </p>
    </div>
  )
}

render(<ControlledExample />)

Collapsible Groups

MenuGroup supports the items declarative API, with animated chevron and open/close transition.

Result
Loading...
Live Editor
render(
  <div style={{ width: 220 }}>
    <Menu defaultValue="files">
      <MenuItem value="files" label="All files" count="(572)" />
      <MenuGroup
        title="Folders"
        items={[
          { value: 'meetings', label: 'Meetings', count: '(199)' },
          { value: 'memos', label: 'Voice memos', count: '(156)' },
          { value: 'reading', label: 'Reading', count: '(48)' },
        ]}
      />
      <MenuGroup
        title="Tags"
        defaultOpen={false}
        items={[
          { value: 'minutes', label: 'Meeting minutes', count: '(24)' },
          { value: 'selling', label: 'Selling plan', count: '(59)' },
        ]}
      />
    </Menu>
  </div>,
)

Group with Action

Pass an action to MenuGroup to render a button in the header (e.g., a "create" button).

Result
Loading...
Live Editor
render(
  <div style={{ width: 220 }}>
    <Menu>
      <MenuGroup
        title="Folders"
        action={
          <button type="button" style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, display: 'flex', alignItems: 'center' }}>
            <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
              <path d="M10.499 9.50049H15.333V10.5005H10.5V15.3335H9.5V10.4995H4.66602V9.49951H9.49902V4.6665H10.499V9.50049Z" fill="currentColor" />
            </svg>
          </button>
        }
        items={[
          { value: 'meetings', label: 'Meetings', count: '(199)' },
          { value: 'memos', label: 'Voice memos', count: '(156)' },
        ]}
      />
    </Menu>
  </div>,
)

Nested Menu

MenuItemEntry.children turns a leaf item into a collapsible parent (any depth). Parent rows toggle on click and do not participate in selection; only leaf items match defaultValue / value.

Result
Loading...
Live Editor
render(
  <div style={{ width: 220 }}>
    <Menu defaultValue="projects/alpha">
      <MenuItem value="home" label="Home" />
      <MenuItem
        value="files"
        label="All files"
        count="(572)"
      />
    </Menu>
    <div style={{ height: 8 }} />
    <div style={{ width: 220 }}>
      <Menu
        items={[
          {
            value: 'projects',
            label: 'Projects',
            defaultOpen: true,
            children: [
              {
                value: 'projects/active',
                label: 'Active',
                defaultOpen: true,
                children: [
                  { value: 'projects/alpha', label: 'Alpha', count: '(3)' },
                  { value: 'projects/beta', label: 'Beta' },
                ],
              },
              { value: 'projects/archive', label: 'Archive' },
            ],
          },
          {
            value: 'tags',
            label: 'Tags',
            children: [
              { value: 'tags/important', label: 'Important' },
              { value: 'tags/personal', label: 'Personal' },
            ],
          },
        ]}
      />
    </div>
  </div>,
)

Size Spec

PropertyTokenValue
Min height40px
Horizontal paddingSpacing_88px
Vertical paddingSpacing_88px
Icon-text gapSpacing_88px
Text-count gapSpacing_44px
Border radiusRadius_55px
Font sizeFont-Size/Body14px
Line heightLine-Height/Body22px
PropertyTokenValue
Min height40px
Padding leftSpacing_88px
Padding rightSpacing_44px
Child items gapSpacing_44px

Color Tokens

StateBackgroundText Color
DefaultLabels/Secondary#3d3d3d
HoverGrays/Gray-1#ebebebLabels/Secondary#3d3d3d
SelectedGrays/Gray-1#ebebebLabels/Secondary#3d3d3d
Group titleLabels/Tertiary#757575
CountLabels/Tertiary#757575

Props

PropTypeDefaultDescription
childrenReactNodeMenu items and groups (composition API)
itemsMenuItemEntry[]Declarative menu item config
headerReactNodeOptional header title
valuestringControlled selected value
defaultValuestringDefault selected value
onValueChange(value: string) => voidSelection change callback
classNamestringCustom className
FieldTypeRequiredDescription
valuestringUnique identifier for selection matching
labelReactNodeItem text
iconReactNodeLeading icon
countReactNodeTrailing count text
actionReactNodeAction element shown on hover
childrenMenuItemEntry[]Sub-items; when non-empty, the entry renders as a collapsible parent (not selectable)
defaultOpenbooleanfalseInitial expanded state when children is non-empty (uncontrolled)
keyKeyReact list key (defaults to value)
classNamestringCustom className
PropTypeDefaultDescription
labelReactNodeItem text (required)
valuestringUnique identifier for selection matching
iconReactNodeLeading icon
countReactNodeTrailing count text
selectedbooleanExplicit selected state (overrides context)
actionReactNodeAction element shown on hover
onClick(e) => voidClick handler
classNamestringCustom className
PropTypeDefaultDescription
titleReactNodeGroup title (required)
itemsMenuItemEntry[]Declarative child items
childrenReactNodeChild menu items (composition API)
actionReactNodeHeader action button
openbooleanControlled open state
defaultOpenbooleantrueDefault open state
onOpenChange(open: boolean) => voidOpen state change callback
classNamestringCustom className