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 height—40px
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 height—40px
Padding leftSpacing_88px
Padding rightSpacing_44px
Child items gapSpacing_44px

Color Tokens​

StateBackgroundText Color
Default—Labels/Secondary#3d3d3d
HoverGrays/Gray-1#ebebebLabels/Secondary#3d3d3d
SelectedGrays/Gray-1#ebebebLabels/Secondary#3d3d3d
Group title—Labels/Tertiary#757575
Count—Labels/Tertiary#757575

Props​

PropTypeDefaultDescription
childrenReactNode—Menu items and groups (composition API)
itemsMenuItemEntry[]—Declarative menu item config
headerReactNode—Optional header title
valuestring—Controlled selected value
defaultValuestring—Default selected value
onValueChange(value: string) => void—Selection change callback
classNamestring—Custom className
FieldTypeRequiredDescription
valuestring✓Unique identifier for selection matching
labelReactNode✓Item text
iconReactNode—Leading icon
countReactNode—Trailing count text
actionReactNode—Action 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)
keyKey—React list key (defaults to value)
classNamestring—Custom className
PropTypeDefaultDescription
labelReactNode—Item text (required)
valuestring—Unique identifier for selection matching
iconReactNode—Leading icon
countReactNode—Trailing count text
selectedboolean—Explicit selected state (overrides context)
actionReactNode—Action element shown on hover
onClick(e) => void—Click handler
classNamestring—Custom className
PropTypeDefaultDescription
titleReactNode—Group title (required)
itemsMenuItemEntry[]—Declarative child items
childrenReactNode—Child menu items (composition API)
actionReactNode—Header action button
openboolean—Controlled open state
defaultOpenbooleantrueDefault open state
onOpenChange(open: boolean) => void—Open state change callback
classNamestring—Custom className