Menu
- Component overview: Sidebar navigation menu for file management and category navigation. Comprises
Menu(container),MenuItem(nav item), andMenuGroup(collapsible group). Supports both declarativeitemsconfig 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 —
Menurenders as<nav>,MenuGroupis built on Radix Collapsible with open/close animation. Selection state managed viavalue/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
MenuItem
| Property | Token | Value |
|---|---|---|
| Min height | — | 40px |
| Horizontal padding | Spacing_8 | 8px |
| Vertical padding | Spacing_8 | 8px |
| Icon-text gap | Spacing_8 | 8px |
| Text-count gap | Spacing_4 | 4px |
| Border radius | Radius_5 | 5px |
| Font size | Font-Size/Body | 14px |
| Line height | Line-Height/Body | 22px |
MenuGroup Header
| Property | Token | Value |
|---|---|---|
| Min height | — | 40px |
| Padding left | Spacing_8 | 8px |
| Padding right | Spacing_4 | 4px |
| Child items gap | Spacing_4 | 4px |
Color Tokens
| State | Background | Text Color |
|---|---|---|
| Default | — | Labels/Secondary#3d3d3d |
| Hover | Grays/Gray-1#ebebeb | Labels/Secondary#3d3d3d |
| Selected | Grays/Gray-1#ebebeb | Labels/Secondary#3d3d3d |
| Group title | — | Labels/Tertiary#757575 |
| Count | — | Labels/Tertiary#757575 |
Props
Menu
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | Menu items and groups (composition API) |
| items | MenuItemEntry[] | — | Declarative menu item config |
| header | ReactNode | — | Optional header title |
| value | string | — | Controlled selected value |
| defaultValue | string | — | Default selected value |
| onValueChange | (value: string) => void | — | Selection change callback |
| className | string | — | Custom className |
MenuItemEntry
| Field | Type | Required | Description |
|---|---|---|---|
| value | string | ✓ | Unique identifier for selection matching |
| label | ReactNode | ✓ | Item text |
| icon | ReactNode | — | Leading icon |
| count | ReactNode | — | Trailing count text |
| action | ReactNode | — | Action element shown on hover |
| children | MenuItemEntry[] | — | Sub-items; when non-empty, the entry renders as a collapsible parent (not selectable) |
| defaultOpen | boolean | false | Initial expanded state when children is non-empty (uncontrolled) |
| key | Key | — | React list key (defaults to value) |
| className | string | — | Custom className |
MenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
| label | ReactNode | — | Item text (required) |
| value | string | — | Unique identifier for selection matching |
| icon | ReactNode | — | Leading icon |
| count | ReactNode | — | Trailing count text |
| selected | boolean | — | Explicit selected state (overrides context) |
| action | ReactNode | — | Action element shown on hover |
| onClick | (e) => void | — | Click handler |
| className | string | — | Custom className |
MenuGroup
| Prop | Type | Default | Description |
|---|---|---|---|
| title | ReactNode | — | Group title (required) |
| items | MenuItemEntry[] | — | Declarative child items |
| children | ReactNode | — | Child menu items (composition API) |
| action | ReactNode | — | Header action button |
| open | boolean | — | Controlled open state |
| defaultOpen | boolean | true | Default open state |
| onOpenChange | (open: boolean) => void | — | Open state change callback |
| className | string | — | Custom className |